Flutter使用MVVM設(shè)計(jì)模式的示例

直奔主題

最開(kāi)始學(xué)習(xí)flutter的時(shí)候,我們可能把ui層和業(yè)務(wù)邏輯層寫(xiě)在了一起,慢慢的dart文件越來(lái)越大,里面的邏輯也越來(lái)越復(fù)雜,然后我們就會(huì)想到,是不是應(yīng)該把代碼重構(gòu)一遍了?
首先,代碼是盡量職責(zé)單一的才好,這樣有問(wèn)題也容易修改,不會(huì)牽一發(fā)而動(dòng)全身,在開(kāi)發(fā)android的時(shí)候,我用過(guò)mvp,用過(guò)mvvm,個(gè)人比較喜歡mvvm,要說(shuō)這兩個(gè)的區(qū)別,首先mvp模式是當(dāng)你獲取到數(shù)據(jù)以后,你需要自己控制如何刷新ui。而mvvm是把數(shù)據(jù)和ui綁定到了一起,當(dāng)你的數(shù)據(jù)改變的時(shí)候,ui自己就會(huì)改變。這個(gè)區(qū)別是個(gè)人的理解,如有錯(cuò)誤請(qǐng)糾正。

然后我們來(lái)說(shuō),如何在flutter中使用mvvm設(shè)計(jì)模式來(lái)讓ui層和業(yè)務(wù)邏輯層解耦,我們先看一段沒(méi)有使用mvvm設(shè)計(jì)模式的代碼,所有代碼已經(jīng)上傳到了github
main.dart

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter MVVM Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomePageNoMVVM(),
    );
  }
}

page_home_no_mvvm.dart

///沒(méi)有使用MVVM設(shè)計(jì)模式的Widget
///author:liuhc
class HomePageNoMVVM extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePageNoMVVM> {
  bool _loading = true;
  String _text;

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

  void loadData() {
    NetWork.query().then((String text) {
      setState(() {
        _loading = false;
        _text = text;
      });
    }).catchError((error) {
      setState(() {
        _loading = false;
        _text = error.toString();
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter沒(méi)有使用MVVM的示例"),
      ),
      body: Center(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              child: Text("點(diǎn)擊重新獲取網(wǎng)絡(luò)數(shù)據(jù)"),
              onPressed: () {
                loadData();
              },
            ),
            Offstage(
              offstage: !_loading,
              child: CircularProgressIndicator(),
            ),
            Expanded(
              child: SingleChildScrollView(
                child: Text("${_text ?? ""}"),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

可以看到,進(jìn)入頁(yè)面的時(shí)候,我們需要請(qǐng)求數(shù)據(jù),獲取到數(shù)據(jù)以后,我們?cè)僬{(diào)用setState刷新頁(yè)面,然后就顯示出來(lái)了獲取到的數(shù)據(jù),這段代碼功能是正常的,但是代碼不是優(yōu)雅的,因?yàn)閡i層既需要控制如何顯示ui,又需要和業(yè)務(wù)層打交道,從業(yè)務(wù)層獲取數(shù)據(jù)后自己再更新ui,這明顯違反了職責(zé)單一的原則,當(dāng)這種邏輯越來(lái)越多,以后維護(hù)就越來(lái)越困難,然后我們來(lái)看一下,如何用mvvm設(shè)計(jì)模式重構(gòu)這段代碼

1. 首先我們創(chuàng)建一個(gè)ViewModel的基類

abstract_base_viewmodel.dart

import 'package:flutter/widgets.dart';

///所有viewModel的父類,提供一些公共功能
///author:liuhc
abstract class BaseViewModel {

  bool _isFirst = true;

  bool get isFirst=>_isFirst;

  @mustCallSuper
  void init(BuildContext context) {
    if (_isFirst) {
      _isFirst = false;
      doInit(context);
    }
  }

  ///獲取數(shù)據(jù)
  @protected
  Future refreshData(BuildContext context);

  @protected
  void doInit(BuildContext context);

  void dispose();
}

這個(gè)類,我封裝了基本所有viewModel都需要的一些方法,那個(gè)init方法的作用是為了保證doInit只執(zhí)行一次,這樣做省去了所有子類都判斷一下是否已經(jīng)執(zhí)行過(guò)init,子類只需要重寫(xiě)doInit就可以保證方法里的代碼只執(zhí)行一次。

2. 然后,我們創(chuàng)建一個(gè)Widget,這個(gè)Widget里,有一個(gè)類屬性為ViewModel的實(shí)例

viewmodel_provider.dart

import 'package:flutter/material.dart';
import 'package:flutter_mvvm/core/abstract_base_viewmodel.dart';

///提供viewModel的widget
///author:liuhc
class ViewModelProvider<T extends BaseViewModel> extends StatefulWidget {
  final T viewModel;
  final Widget child;

  ViewModelProvider({
    @required this.viewModel,
    @required this.child,
  });

  static T of<T extends BaseViewModel>(BuildContext context) {
    final type = _typeOf<ViewModelProvider<T>>();
    ViewModelProvider<T> provider = context.ancestorWidgetOfExactType(type);
    return provider.viewModel;
  }

  static Type _typeOf<T>() => T;

  @override
  _ViewModelProviderState createState() => _ViewModelProviderState();
}

class _ViewModelProviderState extends State<ViewModelProvider> {
  @override
  Widget build(BuildContext context) {
    return widget.child;
  }

  @override
  void dispose() {
    widget.viewModel.dispose();
    super.dispose();
  }
}

3. 完成

是的就是這么簡(jiǎn)單,我們創(chuàng)建了2個(gè)類,就完成了我們的MVVM設(shè)計(jì)模式的框架

4. 使用

下面我們來(lái)看看,如何用這個(gè)mvvm的框架重構(gòu)我們剛才的代碼

4.1 先編寫(xiě)我們的ViewModel類,這里我使用了rxdart,主要是BehaviorSubject可以保存最后一次發(fā)送的數(shù)據(jù),不過(guò)這里沒(méi)有用到這個(gè)特性,你就把它當(dāng)成StreamController就可以了

viewmodel_home.dart

import 'package:flutter/material.dart';
import 'package:flutter_mvvm/core/abstract_base_viewmodel.dart';
import 'package:flutter_mvvm/core/network.dart';
import 'package:rxdart/rxdart.dart';

///首頁(yè)ViewModel類,用來(lái)和業(yè)務(wù)層交互
///author:liuhc
class HomeViewModel extends BaseViewModel {
  // ignore: close_sinks
  BehaviorSubject<String> _dataObservable = BehaviorSubject();

  Stream<String> get dataStream => _dataObservable.stream;

  @override
  void dispose() {
    _dataObservable.close();
  }

  @override
  void doInit(BuildContext context) {
    refreshData(context);
  }

  @override
  Future refreshData(BuildContext context) {
    //個(gè)人比較喜歡這樣寫(xiě),不然要寫(xiě)try catch來(lái)包裹代碼,try catch不如這樣寫(xiě)起來(lái)方便,不用一直定義變量
    return NetWork.query().then((String text) {
      _dataObservable.add(text);
    }).catchError((error) {
      _dataObservable.addError(error);
    });
  }
}
4.2 然后我們來(lái)重構(gòu)首頁(yè)Widget

page_home.dart

import 'package:flutter/material.dart';
import 'package:flutter_mvvm/core/viewmodel_provider.dart';
import 'package:flutter_mvvm/page/home/viewmodel_home.dart';

///使用MVVM設(shè)計(jì)模式的Widget
///author:liuhc
class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  HomeViewModel _viewModel;

  @override
  void initState() {
    super.initState();
    _viewModel = ViewModelProvider.of(context);
    _viewModel.init(context);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter使用MVVM的示例"),
      ),
      body: Center(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              child: Text("點(diǎn)擊重新獲取網(wǎng)絡(luò)數(shù)據(jù)"),
              onPressed: () {
                _viewModel.refreshData(context);
              },
            ),
            Expanded(
              child: SingleChildScrollView(
                child: StreamBuilder(
                  stream: _viewModel.dataStream,
                  builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
                    if (snapshot.connectionState == ConnectionState.waiting) {
                      return Center(
                        child: CircularProgressIndicator(),
                      );
                    }
                    return Text(
                      "${snapshot.hasError ? snapshot.error : snapshot.data}",
                    );
                  },
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

上面代碼的關(guān)鍵部分是通過(guò)ViewModelProvider.of(context);獲取到了上層Widget里的viewModel類實(shí)例,這部分的知識(shí)不是本文的終點(diǎn),不懂的請(qǐng)自己查詢一下相關(guān)知識(shí)。

4.3 然后我們修改程序入口,看一下如何把首頁(yè)Widget首頁(yè)ViewModel綁定到一起的

main.dart

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter MVVM Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: ViewModelProvider(
        viewModel: HomeViewModel(),
        child: HomePage(),
      ),
    );
  }
}

在上面的代碼里,我們的home沒(méi)有直接傳遞HomePage(),而是傳遞的ViewModelProvider,ViewModelProvider的代碼可以在上面發(fā)過(guò)了,在ViewModelProvider這個(gè)類里,我們保存了viewModel的實(shí)例,在ViewModelProviderbuild方法里,我們直接返回了傳入的child,我們還定義了一個(gè)方法static T of<T extends BaseViewModel>(BuildContext context),在這個(gè)方法里通過(guò)調(diào)用context.ancestorWidgetOfExactType找到了該類里的viewModel類屬性,所以在_HomePageState類里我們找到了傳入ViewModelProviderviewModel,然后可以用該viewModel來(lái)進(jìn)行下一步操作。

文章到此講解結(jié)束,在使用該種方式開(kāi)發(fā)的過(guò)程中,還能完美解決TabView隔tab點(diǎn)擊報(bào)錯(cuò)的問(wèn)題(用過(guò)的都知道我在說(shuō)什么),因?yàn)榧词故褂昧薃utomaticKeepAliveClientMixin,挨個(gè)點(diǎn)擊tab的話沒(méi)問(wèn)題,但是隔著點(diǎn)的話還是有問(wèn)題,我也找過(guò)很多方法,都不好用,但是該種方式可以解決該問(wèn)題,因?yàn)槭怯肧treamBuilder刷新的數(shù)據(jù),而ViewModel保存在了上層widget,所以本widget重繪的時(shí)候上層widget的viewModel的實(shí)例并不會(huì)發(fā)生變化,數(shù)據(jù)還在Stream里,所以即使重新執(zhí)行了build方法,也不會(huì)再次聯(lián)網(wǎng)請(qǐng)求數(shù)據(jù),只有我們手動(dòng)給StreamController add數(shù)據(jù)的時(shí)候,才會(huì)將新數(shù)據(jù)給本widget來(lái)進(jìn)行重繪。

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

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

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