abstract class IBaseView { showLoading();
hideLoading(); }
##### 封装Presenter层
`Presenter`层的公共接口只需提供对`View`层的注册以及反注册,如下面代码所示
abstract class IBasePresenter { void onAttachView(V view);
void onDetachView(); }
下面对`Presenter`层的代码实现上面所提供的接口,如下面代码所示
abstract class BasePresenter extends IBasePresenter { V view;
@override void onAttachView(IBaseView view) { this.view = view; }
@override void onDetachView() { this.view = null; } }
##### 封装State基类
在`State`基类中,需要提供`Presenter`的初始化的方法、loading状态、数据初始化以及视图的构建等,如下面代码所示
abstract class BaseState<T extends StatefulWidget, P extends BasePresenter, V extends IBaseView> extends State implements IBaseView { P presenter;
bool isLoading = false;
P initPresenter();
Widget buildBody(BuildContext context);
void initData() { }
@override void initState() { super.initState(); presenter = initPresenter(); if (presenter != null) { presenter.onAttachView(this); } initData(); }
@override void dispose() { super.dispose(); if (presenter != null) { presenter.onDetachView(); presenter = null; } }
@override @mustCallSuper Widget build(BuildContext context) { return new Scaffold( body: buildBody(context), ); }
@override void showLoading() { setState(() { isLoading = true; }); }
@override void hideLoading() { setState(() { isLoading = false; }); } }
到此,`MVP`框架已经封装完,下面只需对登录界面做相应的实现即可。
##### 实现登录逻辑
在进行登录时,`Presenter`层需要向`View`层提供登录接口,当进行登录完毕后,需要向`View`层进行登录状态的反馈,所以`View`需要提供登录成功、失败两个接口,如下面代码所示
abstract class ILoginPresenter extends BasePresenter { void login(String name, String password); }
abstract class ILoginView extends IBaseView { void onLoginSuccess(UserBean userBean);
void onLoginFailed(); }
当相关接口定义完毕后,首先实现登录的`Presenter`层的代码,如下面代码所示
class LoginPresenter extends ILoginPresenter { @override void login(String name, String password) async { if (view != null) { view.showLoading(); } final login = await LoginManager.instance.login(name, password); //授权成功 if (login != null) { final user = await LoginManager.instance.getMyUserInfo(); if (user != null) { if (view != null) { view.hideLoading(); view.onLoginSuccess(user); } else { view.hideLoading(); view.onLoginFailed(); } } } else { if (view != null) { view.hideLoading(); view.onLoginFailed(); } } } }
然后对登录`State`的代码进行实现,如下面代码所示
class _LoginPageState extends BaseState<LoginPage, LoginPresenter, ILoginView> implements ILoginView {
@override void initData() { super.initData(); }
@override Widget buildBody(BuildContext context) { return null; }
@override LoginPresenter initPresenter() { return LoginPresenter(); }
@override void onLoginSuccess(UserBean userBean) { NavigatorUtil.goHome(context, userBean); }
@override void onLoginFailed() { ToastUtil.showToast(‘登录失败,请重新登录’); } }
相关代码已经封装完毕,最后只需调用登录相关逻辑,如下面代码所示
_login() { if (presenter != null) { String name = _nameController.text; String password = _passwordController.text; presenter.login(name, password); } }
BloC
----
关于什么是BloC,可以参考[\[Flutter Package\]状态管理之BLoC的封装]( )和[Flutter | 状态管理探索篇——BLoC(三)]( )。
### 架构视图
![](https://user-gold-cdn.xitu.io/2019/7/15/16bf5068bccb5d8c?imageView2/0/w/1280/h/960/ignore-error/1)
### 程序入口
`main.dart`是程序的入口,完成登录界面的启动,相关代码如下所示
void main() => runApp(BlocApp());
class BlocApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( primaryColor: Colors.black, ), home: BlocProvider( child: LoginPage(), bloc: LoginBloc(), ), ); } }
上面代码跟`MVC`和`MVP`有不同之处,传入`home`的对象是`BlocProvider`,且其包含了`child`和`bloc`实例。如下面代码所示
class BlocProvider extends StatefulWidget { final T bloc; final Widget child;
BlocProvider({ Key key, @required this.child, @required this.bloc, }) : super(key: key);
@override _BlocProviderState createState() { return _BlocProviderState(); }
static T of(BuildContext context) { final type = _typeOf<BlocProvider>(); BlocProvider provider = context.ancestorWidgetOfExactType(type); return provider.bloc; }
static Type _typeOf() => T; }
class _BlocProviderState extends State<BlocProvider> { static final String TAG = “_BlocProviderState”;
@override void initState() { super.initState(); LogUtil.v('initState ’ + T.toString(), tag: TAG); }
@override Widget build(BuildContext context) { LogUtil.v('build ’ + T.toString(), tag: TAG); return widget.child; }
@override void dispose() { super.dispose(); LogUtil.v('dispose ’ + T.toString(), tag: TAG); widget.bloc.dispose(); } }
### 登录流程
BLoC能够允许我们分离业务逻辑,不用考虑什么时候需要刷新屏幕,一切交给StreamBuilder和BLoC就可以完成,所以登录页面继承`StatelessWidget`即可。如下面代码所示
class LoginPage extends StatelessWidget { @override Widget build(BuildContext context) { return StreamBuilder( stream: bloc.stream, initialData: initialData(), builder: (BuildContext context, AsyncSnapshot<LoadingBean> snapshot) { } ); } }
* `stream`代表了这个stream builder监听的流,这里监听的是`LoginBloc`的stream;
* `initData`代表初始的值,因为在首次渲染的时候,还未与用户产生交互,也就不会有事件从流中流出,所以需要给首次渲染一个初始值;
* `builder`函数接收一个位置参数BuildContext和一个snapshot,snapshot就是这个流输出的数据的一个快照,我们可以通过snapshot.data访问快照中的数据,StreamBuilder中的builder是一个AsyncWidgetBuilder,它能够异步构建widget,当检测到有数据从流中流出时,将会重新构建。
#### 创建BloC
首先完成`BloC`基类的封装,基类需要只需要满足登录状态,如下面代码所示
class LoadingBean { bool isLoading; T data;
LoadingBean({this.isLoading, this.data});
@override String toString() { return ‘LoadingBean{isLoading: $isLoading, data: $data}’; } }
abstract class BaseBloc { static final String TAG = “BaseBloc”;
BehaviorSubject _subject = BehaviorSubject();
Sink get sink => _subject.sink;
Stream get stream => _subject.stream;
void dispose() { _subject.close(); sink.close(); } }
#### 创建BloC实例
在登录的`BloC`实例中,完成整个登录过程,我们需要监听账号、密码的输入状态,密码的是否可见状态,以及登录状态,如下面代码所示
class LoginBloc extends BaseBloc<LoadingBean> { LoadingBean bean;
LoginBloc() { bean = LoadingBean( isLoading: false, data: LoginBlocBean( name: ‘’, password: ‘’, obscure: true, ), ); }
changeObscure() { }
changeName(String name) { }
changePassword(String password) { }
login(BuildContext context) async { }
void _showLoading() { bean.isLoading = true; sink.add(bean); }
void _hideLoading() { bean.isLoading = false; sink.add(bean); } }
#### 文本监听
创建账号和密码两个`TextEditingController`实例,并完成其事件监听,如下面代码所示
final TextEditingController _nameController = new TextEditingController(); final TextEditingController _passwordController = new TextEditingController();
LoginBloc bloc = BlocProvider.of(context);
_nameController.addListener(() { bloc.changeName(_nameController.text); }); _passwordController.addListener(() { bloc.changePassword(_passwordController.text); });
当文本发生改变时,会调用`LoginBloc`里相应的改变方法,并对相应的文本进行重新复杂,在通过`sink.add()`更新界面,如下面代码所示
changeName(String name) { bean.data.name = name; sink.add(bean); }
changePassword(String password) { bean.data.password = password; sink.add(bean); }
#### 清空账号输入框
与`MVC`一致,可以参考`MVC`。
#### 密码是否可见
需要改变可见状态,调用`LoginBloc`中的`changeObscure`方法,如下面代码所示
changeObscure() { bean.data.obscure = !bean.data.obscure; sink.add(bean); }
#### 触发登录
需要进行网络请求,控制loading的展示和隐藏,这里需要调用`LoginBloc`中的`login`方法,当登录成功后,则跳转主页展示基本信息,不成功则toast提示,如下面代码所示
login(BuildContext context) async { _showLoading();
final login =
await LoginManager.instance.login(bean.data.name, bean.data.password);
//授权成功
if (login != null) {
final user = await LoginManager.instance.getMyUserInfo();
if (user != null) {
NavigatorUtil.goHome(context, user);
} else {
ToastUtil.showToast('登录失败,请重新登录');
}
} else {
ToastUtil.showToast('登录失败,请重新登录');
}
_hideLoading();
}
Redux
-----
`Redux`是网页开发着广泛使用的设计模式,比如用在React.js中。关于它的介绍可以参考文章[Flutter主题切换之flutter redux]( )。
### 架构视图
![](https://user-gold-cdn.xitu.io/2019/7/15/16bf50695c6d9f68?imageView2/0/w/1280/h/960/format/png/ignore-error/1)
### 程序入口
`main.dart`是程序的入口,完成登录界面的启动,相关代码如下所示
void main() { final store = new Store( appReducer, initialState: AppState.initial(), middleware: [ LoginMiddleware(), ], );
runApp( ReduxApp( store: store, ), ); }
class ReduxApp extends StatelessWidget { final Store store;
const ReduxApp({Key key, this.store}) : super(key: key);
@override Widget build(BuildContext context) { return StoreProvider( store: store, child: StoreConnector<AppState, _ViewModel>( converter: _ViewModel.fromStore, builder: (context, vm) { return MaterialApp( theme: ThemeData( primaryColor: Colors.black, ), home: LoginPage(), ); }, ), ); } }
class _ViewModel { _ViewModel();
static _ViewModel fromStore(Store store) { return _ViewModel(); } }
在程序的入口处,对`Store`进行了初始化工作,完成了对`reducer`、`state`、`middleware`初始化工作。
#### 定义action
完成登录需要有请求登录、请求加载中、请求错误、请求成功等几个状态,如下面代码所示
class FetchLoginAction { final BuildContext context; final String userName; final String password;
FetchLoginAction(this.context, this.userName, this.password); }
class ReceivedLoginAction { ReceivedLoginAction( this.token, this.userBean, );
final String token; final UserBean userBean; }
class RequestingLoginAction {}
class ErrorLoadingLoginAction {}
#### 初始化state
目前只有一个登录功能,所以只需一个登录的state,如下面代码所示
class AppState { final LoginState loginState;
AppState({ this.loginState, });
factory AppState.initial() => AppState( loginState: LoginState.initial(), ); }
class LoginState { final bool isLoading; final String token;
LoginState({this.isLoading, this.token});
factory LoginState.initial() { return LoginState( isLoading: false, token: ‘’, ); }
LoginState copyWith({bool isLoading, String token}) { return LoginState( isLoading: isLoading ?? this.isLoading, token: token ?? this.token, ); } }
#### 初始化reducer
目前只有一个登录功能,所以只需一个登录的reducer,如下面代码所示
AppState appReducer(AppState state, action) { return AppState( loginState: loginReducer(state.loginState, action), ); }
final loginReducer = combineReducers([ TypedReducer<LoginState, RequestingLoginAction>(_requestingLogin), TypedReducer<LoginState, ReceivedLoginAction>(_receivedLogin), TypedReducer<LoginState, ErrorLoadingLoginAction>(_errorLoadingLogin), ]);
#### 初始化middleware
登录的中间件暂时只对其做个简单的初始化过程,如下面代码所示
class LoginMiddleware extends MiddlewareClass { static final String TAG = “LoginMiddleware”;
@override void call(Store store, action, NextDispatcher next) { } }
### 登录流程
Redux能够允许我们分离业务逻辑,不用考虑什么时候需要刷新屏幕,一切交给StoreConnector可以完成,所以登录页面继承`StatelessWidget`即可。如下面代码所示
class LoginPage extends StatelessWidget { @override Widget build(BuildContext context) { return StoreConnector<AppState, LoginPageViewModel>( distinct: true, converter: (store) => LoginPageViewModel.fromStore(store, context), builder: (_, viewModel) => LoginPageContent(viewModel), ); } }
`LoginPageViewModel`只负责登录状态以及登录行为,如下面代码所示
typedef OnLogin = void Function(String name, String password);
class LoginPageViewModel { static final String TAG = “LoginPageViewModel”;
final OnLogin onLogin; final bool isLoading;
LoginPageViewModel({this.onLogin, this.isLoading});
static LoginPageViewModel fromStore( Store store, BuildContext context) { return LoginPageViewModel( isLoading: store.state.loginState.isLoading, onLogin: (String name, String password) { LogUtil.v(‘name is $name, password is $password’, tag: TAG); store.dispatch(FetchLoginAction(context, name, password)); }, ); } }
#### 文本监听
与`MVC`一致,可以参考`MVC`。
#### 清空账号输入框
与`MVC`一致,可以参考`MVC`。
#### 密码是否可见
与`MVC`一致,可以参考`MVC`。
#### 触发登录
在进行登录时,我们只需调用`LoginPageViewModel`内的`onLogin`方法,该方法会通过`store`分发出`FetchLoginAction`,此时中间件`LoginMiddleware`会收到该行为,并对其进行处理。
@override void call(Store store, action, NextDispatcher next) { next(action); if (action is FetchLoginAction) {
|