Flutter项目架构

背景

Flutter作为最近很火的一个跨平台技术,以其高性能、跨平台的一系列优秀特性成功吸引了很多开发者和组织的青睐,但是由于其不同于传统Android或iOS开发的Widget机制,使得视图的代码往往冗长、不够简洁,解决这种困境的方法就是在开发中合理地运用合适的架构模式,使得程序的视图与数据分离,这样视图层的代码只用专心进行视图的描述和操作即可,不涉及过多复杂的数据操作,这样就可以使视图层的代码达到简洁。由于Flutter目前没有官方推荐的项目架构,而且笔者也未遇到大家都说好用的架构模式,故此,笔者基于MVP的架构,设计了一套我个人比较青睐的架构模式,本文将详细介绍,希望可以和大家一起沟通、探索,力争衍生出一套适合Flutter的架构模式,从而大大提高生产力,如果文中有什么地方大家觉得设计的不合理的,大可提出,我们一起讨论。

从实例看模式

本文将基于一个经典的登录/注册需求展开描述,将登录/注册的界面与逻辑分离,以求达到解耦,我们首先想想,对于一个注册/登录需求,按照传统MVP的思想,应该如何将其视图与逻辑分离,大致的流程应该是这样的:

  1. IView、IPresenter、IModel三个接口分别是View、Presenter、Model类应该实现(Implements)的接口
  2. View类中持有IPresenter类的实例,记做IPresenter(实际上是Presenter类的实例,因为Presenter类实现了IPresenter接口),当View中的登录按钮被点击时,调用iPresenter对象的登录方法
  3. Presenter类中持有IView和IModel类的实例,记做iView和iModel(实际上是View类和Model类的实例,因为View类和Model类分别实现了IView接口和IModel接口),当Presenter的登录方法被调用时,调用iModel对象的登录方法
  4. Model类持有IPresenter的实例,记做IPresenter(实际上是Presenter类的实例,因为Presenter类实现了IPresenter接口),Model类的登录方法被调用后,进行网络操作,发起登录请求,获取登录的结果(成功或失败),并调用iPresenter对象的方法将结果回调给Presenter层
  5. Presenter接收到Model的回调之后调用iView对象的回调方法,将结果继续回调给View层进而实现视图更新,整个操作完成

总结一下,IView、IPresenter、IModel三个接口分别应该包含的方法如下:

IView:登录/注册操作、结果回调操作

IPresenter:登录/注册操作、结果回调操作

IModel:登录/注册操作

可以看到,IView、IPresenter、IModel三个接口都具有登录/注册操作,而IView、IPresenter都具有结果回调操作,这样在定义接口的时候,登录/注册操作的声明会被写3遍、结果回调操作的声明会被写2遍,代码上会出现臃肿和冗余。所以我对此进行了改进,改进示意图如下:

将登录/注册操作这类M、V、P都应该具有的操作抽取到IFunction这个接口中,然后让IView、IPresenter、IModel这三个接口实现(implements)IFunction这个接口,将结果回调操作抽取到ICallBack这个接口中,让IView、IPresenter这两个接口实现(implements)这个接口,然后,让View实现(implements)IView接口,让Presenter实现(Implements)IPresenter这个接口,让Model实现(Implements)IModel这个接口,这样,一个登录操作的完成流程是这样的:

  1. View类持有IPresenter类的实例,记做iPresenter(实际上是Presenter类的实例,因为Presenter类实现了IPresenter接口),当View中的登录按钮被点击时,调用iPresenter对象的登录方法
  2. Presenter类中持有IView和IModel类的对象,记做iView和iModel(实际上是View类和Model类的实例,因为View类和Model类分别实现了IView接口和IModel接口),当Presenter类的登录方法被调用时,调用iModel对象的登录方法
  3. Model类持有ICallBack类的实例,记做iCallBack(实际上是Presenter类的实例,因为Presenter类实现了IPresenter接口,而IPresenter接口实现了ICallBack接口),Model类的登录方法被调用后,进行网络操作,发起登录请求,获取登录的结果(成功或失败),并调用iCallBack对象的回调方法将结果回调给iPresenter
  4. Presenter接收到Model的回调之后调用iView对象的实例,将结果继续回调给View层进而实现视图更新,整个操作完成

这样,我们使用IFunction和ICallBack分别将登录/注册操作和结果回调操作进行封装,然后使IView、IPresenter、IModel分别实现自己应该具有的功能,最后让View、Presenter、Model分别实现IView、IPresenter、IModel接口,一步一步实现了视图与数据的解耦。

代码实现

以上是理论分析部分,落实在代码中,我来大家一步一步地实现:

首先定义我们所说的登录/注册操作对应的LoginInterface接口:

enum RequestType { LOGIN, SIGNUP }
class LoginInterface {
 //这里通过RequestType区分当前操作是登录操作还是注册操作
 logInOrSignUp(String userName, String passWord, RequestType requestType) {}
}

然后来定义回调方法对应的ILoginCallBack接口:

class ILoginCallBack {
 //登录/注册成功
 logInOrSignUpSuccess(User user, String describe) {}
 //登录/注册失败
 logInOrSignUpFailed(String describe) {}
}

然后来定义IView接口,因为View应该有登录/注册、回调两个功能,因此应该让IView接口实现LoginInterface、ILoginCallBack两个接口:

class ILoginPage implements LoginInterface,ILoginCallBack {
 showProsess(bool show) {}
 @override
 logInOrSignUp(String userName, String passWord, RequestType requestType) {
 // TODO: implement logInOrSignUp
 return null;
 }
 @override
 logInOrSignUpFailed(String describe) {
 // TODO: implement logInOrSignUpFailed
 return null;
 }
 @override
 logInOrSignUpSuccess(User user, String describe) {
 // TODO: implement logInOrSignUpSuccess
 return null;
 }
}

注意,这里只是定义接口,所以方法不用实现。

定义IPresenter接口,由于Presenter应该有登录/注册、回调两个功能,因此应该让IView接口实现LoginInterface、ILoginCallBack两个接口:

class ILoginPagePresenter implements LoginInterface, ILoginCallBack {
 @override
 logInOrSignUp(String userName, String passWord, RequestType requestType) {
 // TODO: implement logInOrSignUp
 return null;
 }
 @override
 logInOrSignUpFailed(String describe) {
 // TODO: implement logInOrSignUpFailed
 return null;
 }
 @override
 logInOrSignUpSuccess(User user, String describe) {
 // TODO: implement logInOrSignUpSuccess
 return null;
 }
}

最后定义IModel接口,其只需要登录/注册一个功能即可,因此让IModel接口实现Logininterface这一个接口即可:

class ILoginModel implements LoginInterface {
 @override
 logInOrSignUp(String userName, String passWord, RequestType requestType) {
 // TODO: implement logInOrSignUp
 return null;
 }
}

然后我们来实现View、Presenter、Model三个类:

View:

class _LoginState extends State<LoginPage> implements ILoginPage{
 ILoginPagePresenter iLoginPagePresenter;
 @override
 void initState() {
 iLoginPagePresenter = LogInPresenter(this);
 super.initState();
 }
 @override
 logInOrSignUp(String userName, String passWord, RequestType requestType) {
 iLoginPagePresenter.logInOrSignUp(userName, passWord, requestType);
 }
 @override
 logInOrSignUpFailed(String describe) {
 // TODO: 登录/注册失败,更新视图
 }
 @override
 logInOrSignUpSuccess(User user, String describe) {
 // TODO: 登录/注册成功,更新视图
 }
}

Presenter:

class LogInPresenter implements ILoginPagePresenter {
 ILoginPage _iLoginPage;
 ILoginModel _iLoginModel;
 LogInPresenter(this._iLoginPage) {
 _iLoginModel = LoginModel(this);
 }
 @override
 logInOrSignUp(String userName, String passWord, RequestType requestType) {
 _iLoginModel.logInOrSignUp(userName, passWord, requestType);
 }
 @override
 logInOrSignUpFailed(String describe) {
 _iLoginPage.logInOrSignUpFailed(describe);
 }
 @override
 logInOrSignUpSuccess(User user, String describe) {
 _iLoginPage.logInOrSignUpSuccess(user, describe);
 }
}

Model:

class LoginModel implements ILoginModel {
 ILoginCallBack _iLoginCallBack;
 LoginModel(this._iLoginCallBack);
 @override
 logInOrSignUp(String userName, String passWord, RequestType requestType) {
 // TODO: 发起网络操作进行登录/注册
 _iLoginCallBack.logInOrSignUpSuccess(user, describe);
 }
}

至此我们就对登录/注册这个功能完成了架构改造,可以发现,经过架构改造之后,整个项目的条理非常清晰,避免了视图层代码太过冗余的痛点,利于维护、测试。

总结

本文针对Flutter中的登录/注册场景进行了架构改造,整个逻辑是基于MVP,解决了MVP中接口冗余的痛点,当然本架构还存在很多问题,欢迎大家及时指出,一起交流。