MVVM在Flutter中的實現(xiàn)


什么是MVVM

很多文章也介紹過了,那就說一說自己對它的理解吧。MVVM是如今主流的應用架構(gòu),它將我們的應用結(jié)構(gòu)分為M(Model)V(View)、VM(ViewModel),下面對這三部分進行簡單的描述。

M-Model層

數(shù)據(jù)倉庫層,你可以把你獲取數(shù)據(jù)的方法(不管是從服務(wù)器還是從本地,調(diào)用者不關(guān)心數(shù)據(jù)是從哪里來的,數(shù)據(jù)來源規(guī)則在自己內(nèi)部定義)、數(shù)據(jù)模型(各種entity、bean什么的)都放在這一層。

V-View層

表示層,一切用戶能看到的東西都在這層;比如頁面、彈窗、按鈕、Toast等等,并接收一切用戶輸入,我們將所有和UI相關(guān)的代碼都放在這一層。

VM-ViewModel層

業(yè)務(wù)邏輯層,我稱它為中間層。它負責從Model層獲取數(shù)據(jù),又為綁定它的View提供量身定制的數(shù)據(jù),它連接ViewModel并為View提供服務(wù)。ViewModel既要響應View的輸入,又要在數(shù)據(jù)更新后通知View,它其實對于View來說就是一個被觀察者,在整個View的生命周期中一直被View關(guān)注。


為什么推薦使用MVVM構(gòu)建項目

若不使用一個好的架構(gòu)去構(gòu)建項目,當我們的項目逐漸龐大,維護起來是非常痛苦的。前不久就作死的重構(gòu)過一個3000行代碼的頁面,State里面各種變量、各種網(wǎng)絡(luò)請求、各種狀態(tài)、各種setState,那滋味別提多酸爽。早些年的MVCMVP架構(gòu)在這里就不贅述了,MVVM是由它們演變而來的,實現(xiàn)了ViewModel全解耦?;谶@個特點它的好處太多了;比如以后我們可以基于每個ViewModel編寫單元測試了、更細粒度的控制View的刷新以提高性能等等。


實現(xiàn)一個簡單的MVVM

這里實現(xiàn)一個簡陋的登陸案例,創(chuàng)建如下結(jié)構(gòu)的文件


user_entity.dart用戶屬性數(shù)據(jù)模型

class UserEntity {
  String userId;
  String userName;
  int age;

  UserEntity.fromJson(Map json){
    userId = json['userId'];
    userName = json['userName'];
    age = json['age'];
  }
}

login_model.dart請求登陸接口、獲取數(shù)據(jù)相關(guān)

class LoginModel {
  Future<UserEntity> login(String account,String password) async {
    ///模擬網(wǎng)絡(luò)請求并解析成功
    await Future.delayed(Duration(seconds: 3));
    return UserEntity.fromJson({'userId':'9527','userName':'愛靜靜真好','age':18});
    /*http.Response resp = await http.post('https://www.baidu.com/login',body: {'account':account,'password':password});
    if(resp.statusCode == 200){
      return UserEntity.fromJson(jsonDecode(resp.body));
    }
    throw '登陸失敗';*/
  }
}

login_widget.dart登陸頁面

class LoginWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _LoginPageState();
}

class _LoginPageState extends State {
  LoginViewModel loginViewModel;
  TextEditingController accountCtrl, passwordCtrl;

  BuildContext dialogCtx;

  @override
  void initState() {
    super.initState();

    ///初始化LoginViewModel
    loginViewModel = LoginViewModel();
    accountCtrl = TextEditingController();
    passwordCtrl = TextEditingController();

    ///監(jiān)聽登陸狀態(tài)展示loading路由
    loginViewModel.state.addListener(() async {
      ///登陸中展示loading
      if (loginViewModel.state.value == LoginState.ing) {
        await showDialog(
            context: context,
            builder: (ctx) {
              dialogCtx = ctx;
              return AlertDialog(content: Text('登陸中...'),);
            });
      } else {
        ///登陸結(jié)束dismiss彈窗
        if (ModalRoute.of(dialogCtx).isCurrent) {
          Navigator.maybePop(dialogCtx);
        }
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ///這里有個Text需要顯示登陸成功后的userName
            ChangeNotifierWidget<LoginViewModel>(loginViewModel, (ctx,vm){
              return Text(vm.userName);
            }),
            TextField(
              controller: accountCtrl,
            ),
            TextField(
              controller: passwordCtrl,
            ),
            FlatButton(onPressed: () => loginViewModel.login(accountCtrl.text, passwordCtrl.text), child: Text('登陸')),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    super.dispose();

    ///有些viewmodel可能需要釋放
    loginViewModel.dispose();
  }
}

login_view_model.dart登陸頁面的ViewModel

///登陸過程的狀態(tài)
enum LoginState{
  init,
  ing,
  success,
  fail,
}

class LoginViewModel extends ChangeNotifier {
  String userName;
  ///登陸狀態(tài)
  ValueNotifier<LoginState> state;

  LoginModel model;
  LoginViewModel(){
    userName = '';
    model = LoginModel();
    state = ValueNotifier(LoginState.init);
  }

  ///登陸響應
  void login(String account,String password) async {
    state.value = LoginState.ing;
    ///這里可以做校驗
    if(account == null || password == null){
      state.value = LoginState.fail;
      return;
    }
    try {
      UserEntity entity = await model.login(account, password);
      userName = entity.userName;
      state.value = LoginState.success;
    } catch (e){
      state.value = LoginState.fail;
    } finally {
      notifyListeners();
    }
  }
}

然后為了實現(xiàn)細粒度的刷新,編寫了一個簡單的基于ChangeNotifier機制的Widget

///一個簡單的ChangeNotifierWidget,可以監(jiān)聽ChangeNotifier的notifyListeners方法進行重建,
///這里的ChangeNotifier其實就是我們的ViewModel
typedef MyConsumer<T> = Widget Function(BuildContext ctx,T vm);

class ChangeNotifierWidget<T extends ChangeNotifier> extends StatefulWidget {
  final T viewModel;
  final Widget Function(BuildContext,T vm) builder;

  ChangeNotifierWidget(this.viewModel, this.builder);

  @override
  State<StatefulWidget> createState() => _ChangeNotifierState<T>();
}


class _ChangeNotifierState<T extends ChangeNotifier> extends State<ChangeNotifierWidget<T>> {
  @override
  void initState() {
    super.initState();
    // ignore: invalid_use_of_protected_member
    assert(!widget.viewModel.hasListeners, 'this ChangeNotifier has more than one listeners!');
    widget.viewModel.addListener(() {
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    T vm = widget.viewModel;
    return widget.builder(context,vm);
  }

  @override
  void dispose() {
    super.dispose();
    ///注意,這里Widget和ViewModel綁定,理論上來說當這個[StatefulWidget]對應的[StatefulElement]被移除的時候,
    ///與他綁定的ViewModel應該也被銷毀,所以這里調(diào)用了ChangeNotifier.dispose()
    ///但是?。?!還有種情況是,可能存在一個祖先ViewModel,這個Widget需要監(jiān)聽祖先ViewModel的數(shù)據(jù)變化,那么這里就不應該被銷毀,這里暫時不考慮這種情況
    widget.viewModel.dispose();
  }
}

ok到這里代碼就寫完了,跑起來


可以看到,點擊登陸顯示出了loading彈窗,因為LoginViewModel里的state這個ValueNotifier的值被改成了LoginState.ing了,這時候View層監(jiān)聽到了這個值的變化,所以調(diào)用showDialog展示彈窗。然后我們在Model層用3秒延遲模擬了網(wǎng)絡(luò)請求然后返回假數(shù)據(jù),ViewModel層拿到結(jié)果后更新userName,最后調(diào)用notifyListeners通知View層,完成ViewuserName的顯示。


寫在最后

本文只是舉了一個比較簡單的例子,頁面也很簡單,其實并不是一個頁面就對應一個ViewModel類,它可以引用多個ViewModel,而引用多個ViewModel往往能帶來更多的好處,比如更細粒度的控制頁面中子Widget的更新、各個ViewModel實現(xiàn)自己的業(yè)務(wù)而不用把所有數(shù)據(jù)都放在頁面級這個ViewModel里面;有時可能會有一個子Widget完成了他的業(yè)務(wù)邏輯后就從這個頁面的element樹上被移除了的需求,這時候如果能為這個子Widget綁定一個專屬于它的ViewModel并且跟隨Widget的生命周期,那么對性能也是一種提升。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容