介绍
当您尝试编写应用程序时,遇到的第一件事是如何组织应用程序的体系结构。当涉及Flutter时,头脑可以完全绕过Google提供的功能-香草,范围模型,BLoC,MVP,MVC,MVVM,MVI等。假设您决定采用最时尚的方式(谷歌在2018年建议的方式)使用BLOC。它是什么?如何使用它?还是Redux或RxDart?-虽然停下来是关于“其他”的事情……但是,接下来该怎么办?要连接哪些库?Bloc,Flutter_bloc,bloc_pattern等?如此众多的体系结构选项和用于实现它们的工具实际上会长时间延迟选择阶段。给谁的文章
对于那些刚刚开始学习Flutter并且不知道从哪里开始的人来说,本文将是非常有用的。我将展示在Flutter上实现应用程序的选项之一。这将使您“感觉” Flutter,然后自己决定编写应用程序的方式和使用方式。模式和工具。简短而简单
所以,让我们开始吧。值得注意的第一件事是,存在一个应用程序架构(模式,模板,某些构造概念)-完全相同:BLoC,MVP,MVC,MVVM,MVI等。这些架构中的许多不仅在Flutter中使用,而且在其他编程语言中使用。问题-有什么选择?我认为,您需要选择自己熟悉的知识,但前提是这需要反应性并将业务逻辑与界面严格分开(是的,是的-“如果汽车是黑色,则可以是任何颜色”)。至于界面和业务逻辑的分离,我认为没有必要进行解释,但是对于反应性-试试,如果您还没有尝试过-最后,它真的非常方便且“漂亮”。如果您自己不能选择它,那么让我们允许不是Google最高才的家伙-BLOC为我们完成它。我们找出了架构。现在,工具-有现成的库-Bloc,Flutter_bloc,bloc_pattern-哪个更好?我不知道-每个人都很好。您可以选择和比较很长一段时间,但是就像在军队中一样,这里又是一次-最好现在就做出错误的决定,而不要做出任何决定。现在,我建议回到mod之后并使用Provider(在2019年,同样的人建议使用Provider)。所有这些将使我们能够根据需要创建全局块和局部块。关于BLoC的体系结构(即模式,而不是库)已经写了很多文章,我认为您不应该再详细介绍它。我只注意到本文的一点,不是使用经典的BLoC,而是稍作修改-在BLoC中,动作(事件)不会通过接收器进行传输,但是会调用BLoC函数。简而言之,此刻我看不到使用水槽的好处-既然它们不存在,那为什么使您的生活变得复杂?Dart中的异步和并行计算
因为我们在谈论反应性,所以在Dart中还需要对异步的概念进行一些澄清。很多时候,在接触Dart的最初阶段,异步函数(async)的含义无法正确理解。您应该始终记住,“默认情况下”该程序在一个线程中运行,并且异步仅允许您更改命令的顺序,而不是并行执行它们。也就是说,如果仅通过将其标记为异步来简单地运行带有大量计算的函数,那么该接口将被阻塞。异步不会启动新线程。异步和等待的工作方式在Internet上有很多信息,因此我也不再赘述。如果需要进行一些大的计算并且同时又不阻塞接口,则需要使用计算功能(对于特殊的硬核,可以使用隔离)。这实际上将启动一个单独的执行线程,该线程还将具有其自己的单独的内存区域(这很令人难过)。您只能通过包含简单数据类型及其列表的消息与此类流进行通信。让我们开始练习
问题的提法
让我们尝试编写最简单的应用程序-让它成为某种电话目录。我们将使用Firebase作为存储-这将使我们能够创建“云”应用程序。我将跳过如何将Firebase连接到项目的操作(有关该主题的文章已写了多篇,我看不到重复的意思。注意:该项目中使用Cloud Firestore。)。应该是这样的:
应用说明
我们的应用程序将在外部包含:- Firebase授权窗口(此窗口的逻辑将包含在MainBloc中)。
- 信息窗口-将显示有关授权程序的用户的信息(该窗口的逻辑也将包含在MainBloc中)。
- 电话列表形式的目录窗口(此窗口的逻辑将包含在单独的PhonebookBloc中)。
- 可以切换屏幕的应用程序菜单。
内部应用程序的构造如下:每个屏幕将包含一个带有屏幕小部件的文件,一个bloc文件(具有相应的bloc类),一个动作文件(包含描述影响bloc状态的事件的简单类),一个状态文件(包含反映bloc状态的简单类) ),包含存储库类(负责接收数据)和数据类(存储集团业务逻辑数据)的data_model文件。该应用程序将具有以下功能:打开屏幕时,将使用初始状态值初始化相应的块,并在必要时在块构造器中调用一些初始动作。屏幕根据状态构建/重建,返回状态。用户在应用程序中执行一些具有相应动作的动作。动作被传递给bloc类,在mapEventToState函数中对其进行处理,并且bloc将新状态返回到屏幕,并以此为基础重建屏幕。档案结构
首先,我们创建一个空的Flutter项目,并创建这种项目结构(我注意到在演示项目中,某些文件最终将保持为空):
授权窗口。主块
现在,您需要在Firebase中实施授权。让我们从创建事件类(通过bloc中的事件传输数据很方便)和Main bloc的状态开始:文件MainBloc \ actionabstract class MainBlocAction{
String get password => null;
String get email => null;
}
文件MainBloc \国家abstract class MainBlocState{
bool busy;
MainBlocState({this.busy = false});
copy(bool busy) {
return null;
}
}
状态类中的busy标志用于在界面中显示progress_hud,并在滚动列表时排除从数据库读取的不必要数据。在该块中的所有操作开始之前,将设置了忙标志的旧类型的新状态发布到输出流-这样,接口将收到操作已开始的通知。在操作结束时,将清除忙碌标志后将新状态发送到流。MainBlocState类的继承人描述了主应用程序Bloc的状态。MainBlocAction的继承人描述了其中发生的事件。MainBloc类包含4个主要元素-将事件“转换”为状态(Future mapEventToState)的功能,Bloc状态为_blocState,bloc状态存储库为repo,“输出”状态流(接口元素跟踪)为blocStream。基本上,这些都是提供bloc-a功能的元素。有时建议在一个块中使用2个输出流-这样的例子将更少。我不会在这里列出它-您可以通过下载项目来查看它。bloc存储库类包含用于处理Firebase的逻辑和一个对象(数据),该对象存储该bloc实现的业务逻辑所需的数据。文件MainBloc \ data_modelclass MainRepo{
final MainData data = MainData();
FirebaseAuth get firebaseInst => MainData.firebaseInst;
FirebaseUser _currentUser;
Future<bool> createUserWithEmailAndPassword(
String email, String password) async {
var dataUser;
try {
dataUser =
(await firebaseInst.createUserWithEmailAndPassword(
email: email, password: password))
.user;
} catch (e) {
print(Error.safeToString(e));
print(e.code);
print(e.message);
}
if (dataUser == null){
data.setState(IsNotLogged());
return false;
}
_currentUser = dataUser;
data.setState(IsLogged(),
uid: _currentUser.uid,
email: _currentUser.email);
return true;
}
...}
class MainData {
static final firebaseInst = FirebaseAuth.instance;
static MainBlocState _authState = IsNotLogged();
static MainBlocState get authState => _authState;
static String _uid;
static String get uid => _uid;
static String _email;
static String get email => _email;
void setState(MainBlocState newState,
{String uid = '', String email = ''}) {
_authState = newState;
_uid = uid;
_email = email;
}
}
MainData类还存储状态,但将授权状态存储在Firebase中,而不存储Bloc状态。我们为主要集团编写了逻辑,现在我们可以开始实现授权/注册屏幕了。MainBloc在主文件中初始化:主文件class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider(
create: (context) => MainBloc(),
dispose: (context, value) => value.dispose(),
child: MaterialApp(
routes: menuRoutes,
));
}
}
现在该讨论一下StreamBuilder,Provider,StreamProvider,Consumer和Selector。关于供应商的退却
提供程序 -仅将存储的值沿树传送。而且您只能在孩子建立后访问它,即您需要构建一个子窗口小部件。不负责更新小部件。StreamBuilder-监视流的小部件,当它从流中接收到新对象时将对其进行完全重建。StreamProvider-一个监视流的小部件,并在接收到新对象时发出信号,指示应重建子小部件(那些通过build方法声明为单独类的小部件)。消费者和选择者是“语法糖”,即这实际上是一个“包装”,其中包含构建并将其隐藏在其下。在Selector-e中,您可以进行其他更新过滤。因此,当您需要在每次事件中重建大部分屏幕时,可以将此选项与Provider和StreamBuilder一起使用。当需要重建靠近叶子的窗口小部件树的部分时,建议将StreamProvider与Consumer和Selector结合使用,以排除树的不必要重建。授权书 延续性
进入应用程序时,用户必须进入授权/注册窗口,并且此时应用程序菜单尚不可用。第二点-部分刷新此屏幕没有多大意义,因此我们可以使用StreamBuilder来构建界面。该项目的第三点是使用导航器在屏幕之间导航。收到成功授权事件后,有必要调用转换到信息屏幕。但是,仅在内部构建StreamBuilder中,这将无法工作-将会出现错误。为了解决这个问题,你可以使用辅助包装类StreamBuilderWithListener(尤金Brusov - stackoverflow.com)。现在,此屏幕的列表为auth_screen本身(我将在此处进行部分介绍):文件auth_screenWidget build(BuildContext context) {
var bloc = Provider.of<MainBloc>(context, listen: false);
return StreamBuilderWithListener<MainBlocState>(
stream: bloc.blocStream.stream,
listener: (value) {
if (value is IsLogged) {
Navigator.of(context).pushReplacementNamed(InfoScreen.nameMenuItem);
}
},
initialData: bloc.state,
builder: (context, snappShot) {
if (snappShot.data is IsLoggedOnStart) {
return LoggedWidget();
} else if (snappShot.data is IsLogged) {
return ModalProgressHUD(
inAsyncCall: true,
child: Text(''),);
} else if (snappShot.data is IsNotLogged) {
return SignInAndSignUpWidget();
}
return Scaffold(body: Text(" Unknown event"));
});
}
首先,创建一个StreamBuilderWithListener来侦听来自bloc的流。然后根据当前状态,调用LoggedWidget小部件(如果用户已经登录)或SignInAndSignUpWidget(如果用户尚未登录)。如果bloc返回IsLogged状态,则在构建器中不会发生使用Navigator切换到新屏幕的情况(这会导致错误),但在侦听器中不会发生。在基础小部件中,基于此处返回的数据构建接口。在这里,实际上使用了Provider + StreamBuilder捆绑包,因为 当块的状态更改时,实际上整个接口都会更改。要将数据传输到块,请使用TextEditingController和action参数:auth_screen文件class _SignUpWidgetWidgetState extends State {
String _email, _password;
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override
void initState() {
super.initState();
_emailController.addListener(_onEmailChanged);
_passwordController.addListener(_onPasswordChanged);
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'email'),
),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'password'),
),
RaisedButton(
child: Text('sign up'),
onPressed: () {
Provider.of<MainBloc>(context, listen: false).mapEventToState(
Registration(_email, _password));
})
],
);
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
void _onEmailChanged() {
_email = _emailController.text;
}
void _onPasswordChanged() {
_password = _passwordController.text;
}
}
电话簿屏幕窗口
现在,让我们谈谈PhoneBookScreen窗口。这是最有趣的窗口-这里的界面是基于来自bloc的2个流构建的,并且还有一个带有滚动和分页(pagination)的列表。PhonebookScreen \屏幕文件class PhonebookTopPart extends StatelessWidget {
StatefulWidget caseWidget(PhonebookState state) {
if (state is PhonebookListOpened) {
return PhonebookList();
}else ModalProgressHUD(
inAsyncCall: true,
child: Text(''),);
return null;
}
@override
Widget build(BuildContext context) {
var bloc = Provider.of<PhonebookBloc>(context, listen: false);
return StreamProvider<PhonebookState>(
create: (context) => bloc.blocStream.stream,
initialData: bloc.state,
child: Selector<PhonebookState,PhonebookState>(
selector: (_,state)=>state,
shouldRebuild: (previous, next){return (previous.runtimeType!=next.runtimeType);},
builder: (_, state, __) { return ModalProgressHUD(
inAsyncCall: state.busy,
child: Scaffold(
appBar: AppBar(
title: Text("Phones list"),
),
drawer: MenuWidget(),
body: caseWidget(state),
));}
));
}
}
需要第一个StreamProvider在目录的不同屏幕之间切换-列表,联系人卡片,用于编辑的联系人卡片等。在caseWidget函数中选择了用于屏幕的窗口小部件(但在此示例中,仅实现了列表的视图-您可以尝试实现联系人卡的视图-这非常简单,并且从一个好的开始就可以)。在此屏幕上,已经使用了一堆StreamProvider + Selector / Consumer,因为列表有一个滚动条,建议不要用它来重建整个屏幕(即,重建小部件来自相应的选择器/使用者,并且位于树的下部)。这是列表本身的实现:PhonebookScreen \ screen文件class _PhonebookListState extends State<PhonebookList> {
ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_scrollController.addListener(_scrollListener);
}
@override
Widget build(BuildContext context) {
var bloc = Provider.of<PhonebookBloc>(context, listen: false);
var list = bloc.repo.data.list;
return Container(
child: StreamProvider<PhonebookState>(
create: (context) => bloc.scrollStream.stream,
initialData: bloc.scrollState,
child: Consumer<PhonebookState>(
builder: (_, state, __) {
return ListView.builder(
controller: _scrollController,
itemCount: list.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(list[index].data['name']),
subtitle: Text(list[index].data['phone']),
);
});
},
)));
}
void _scrollListener() {
double delta = MediaQuery
.of(context)
.size
.height * 3;
double maxScroll = _scrollController.position.maxScrollExtent;
double currentScroll = _scrollController.position.pixels;
if (maxScroll - currentScroll <= delta) {
Provider.of<PhonebookBloc>(context, listen: false)
.mapEventToState(ScrollPhonebook());
}
}
@override
void dispose() {
_scrollController.removeListener(_scrollListener);
super.dispose();
}
}
在这里,我们看到第二个StreamProvider,它监视负责滚动的第二个bloc流。分页是通过_scrollListener(控制器:_scrollController)进行标准组织的。尽管该窗口很有趣,但是由于给出了第一个窗口的详细说明,因此这里无话可说了。因此,就是今天。本文的目的不是展示完美的代码,也就是说,您可以在这里找到许多优化点-正确地“拆分”为文件,使用实例,mixin等。另外,下一步是“乞求”什么-您可以制作联系卡。主要任务是构造知识,为构建应用程序设置一定的向量,对在Flutter上设计应用程序的某些时刻进行说明,这些认识在相识的最初阶段并不十分明显。可以在以下位置下载该项目(注册时,您可以使用密码至少为6个字符的任何邮件。重新授权时,密码必须与注册时使用的密码相同)。