flutter mvvm實(shí)現(xiàn)demo

去年10月分開始用flutter做項(xiàng)目存炮,剛剛接觸也沒有怎么做架構(gòu),直接mvc就上手了蜈漓,磕磕碰碰的終于也算是把項(xiàng)目做上線了」回過頭來(lái)再看代碼融虽,一大堆亂七八糟的代碼耦合在一起,各種重復(fù)代碼灼芭,自己都有點(diǎn)嫌棄有额,我想以后入坑的同學(xué)一定會(huì)罵死我,為此決定使用mvvm重構(gòu)彼绷。github地址

mvvm項(xiàng)目結(jié)構(gòu):

mvvm結(jié)構(gòu).png
app_manager:管理類

AppManager: 公共參數(shù)
AppRoutesManager: 路由管理
ListenerManager: 監(jiān)聽管理

base:基類

BaseState: 繼承自State巍佑,用來(lái)處理一些公共的功能比如:友盟統(tǒng)計(jì),公共的UI界面
BaseViewModel: viewModel基類寄悯,包含一些公共的參數(shù)萤衰,函數(shù)
ResponseModel: 一個(gè)model類

config:配置文件

網(wǎng)絡(luò)路徑,各種平臺(tái)參數(shù)猜旬,api等

network:網(wǎng)絡(luò)請(qǐng)求封裝
page:界面
utils:工具類
widgets:公共組件

mvvm之viewModel:

這里使用到了flutter的ChangeNotifier類脆栋,它可以在數(shù)據(jù)改變的時(shí)候調(diào)用notifyListeners()函數(shù)通知組件更新狀態(tài)倦卖。

BaseViewModel

isLoading:當(dāng)它true的時(shí)候顯示加載動(dòng)畫組件,為false的時(shí)候顯示正常的UI組件椿争;
refreshData:我們需要在子類里面實(shí)現(xiàn)它怕膛,主要是用來(lái)加載數(shù)據(jù),它有一個(gè)參數(shù)isShowLoading默認(rèn)為true秦踪,也就是調(diào)用這個(gè)函數(shù)時(shí)我們默認(rèn)顯示加載動(dòng)畫組件褐捻。

import 'package:flutter/widgets.dart';

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

  BaseViewModel(this.context);

  BuildContext context;
  //是否正在加載
  bool _isLoading = false;
  bool get isLoading => _isLoading;
  set isLoading(bool isLoading) {
    if(_isLoading!=isLoading){
      _isLoading= isLoading;
      this.notifyListeners();
    }
  }
  ///刷新數(shù)據(jù)
  @protected
  Future refreshData({bool isShowLoading = true});
}
HomeViewModel

HomeViewModel里面我們實(shí)現(xiàn)refreshData()函數(shù)椅邓,并notifyListeners()函數(shù)通知組件刷新狀態(tài)柠逞。

import 'package:flutter/material.dart';
import 'package:flutter_sg_mvvm/base/base_view_model.dart';
import 'package:flutter_sg_mvvm/page/home/model/product_model.dart';
import 'package:flutter_sg_mvvm/page/home/services/home_services.dart';
import 'package:flutter_sg_mvvm/widgets/loading_dialog/loading_dialog.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:rxdart/rxdart.dart';

class HomeViewModel extends BaseViewModel {
  HomeViewModel(BuildContext context) : super(context);

  // 頁(yè)數(shù)
  int _page = 0;
  int get page => _page;
  set page(int page) {
    _page = page;
  }

  List <ProductModel> _dataList = [];
  List<ProductModel> get dataList => _dataList;
  set dataList(List<ProductModel> arr) {
    _dataList = arr;
    this.notifyListeners();
  }

  RefreshController _refreshController = RefreshController(initialRefresh: false);
  RefreshController get refreshController => _refreshController;

  @override
  Future refreshData({bool isShowLoading = false}) {

    this.isLoading = isShowLoading;

    HomeServices.getHomeList().then((data){
      if(_page==0){
        _dataList.clear();
        _refreshController.refreshCompleted();
      }else{
        _refreshController.loadComplete();
      }

      _dataList.addAll(data) ;

      if(_dataList.length>=30){
        _refreshController.loadNoData();
      }else{
        _refreshController.resetNoData();
      }
      _page++;
      this.isLoading = false;
      this.notifyListeners();

    }).catchError((e){
      print("獲取首頁(yè)列表異常$e");
    });
  }

  //收藏或取消收藏
  Future collectionProduc(int index){
    LoadingDialog.showLoadingDialog(context);
    Future.delayed(Duration(milliseconds: 2000),(){
      ProductModel model = _dataList[index];
      model.isCollection = !model.isCollection;
      this.notifyListeners();
      LoadingDialog.cancelLoadingDialog(context);
    });
  }
}

mvvm之view:

BaseState

這個(gè)類我們繼承自State,這里我開始的目的是為了不想友盟界面統(tǒng)計(jì)的時(shí)候在每個(gè)界面寫統(tǒng)計(jì)的代碼希坚,后來(lái)又加了泛型將viewModel傳了進(jìn)來(lái)边苹,一部分公共的UI處理邏輯initView(),利用viewModel.isLoading來(lái)控制組件的顯示裁僧。

import 'package:flutter/material.dart';
import 'package:flutter_sg_mvvm/app_manager/app_routes_manager.dart';
import 'package:flutter_sg_mvvm/base/base_view_model.dart';
import 'package:flutter_sg_mvvm/widgets/loading_widget/loading_widget.dart';
import 'package:provider/provider.dart';

abstract class BaseState<T extends StatefulWidget, E extends BaseViewModel> extends State<T> {
  String pageName;
  E viewModel;
  void initState() {
    // TODO: implement initState
    super.initState();
    Future.delayed(Duration(microseconds: 500), () {
      print("initState 進(jìn)入${pageName}界面");
    });
  }

  @override
  Widget build(BuildContext context) {
    return build(context);
  }

  @override
  void didChangeDependencies() {
    // TODO: implement didChangeDependencies
    super.didChangeDependencies();
    print("didChangeDependencies: ${pageName}界面");
  }

  @override
  void didUpdateWidget(StatefulWidget oldWidget) {
    // TODO: implement didUpdateWidget
    super.didUpdateWidget(oldWidget);
    print("didUpdateWidget: ${pageName}界面");
  }

  @override
  void deactivate() {
    // TODO: implement deactivate
    super.deactivate();
    print("deactivate:${pageName}界面");
  }

  //公共UI處理个束,如不想使用公共部分,直接在子類重寫initView()函數(shù)
  Widget initView() {
    return Consumer<E>(
      builder: (build, provide, _) {
        print('Consumer-initView');
        print('${viewModel.isLoading}');
        return viewModel.isLoading ? LoadingWidget() : buildView();
      },
    );
  }

  //不同部分UI處理聊疲,在子類必須實(shí)現(xiàn)buildView()函數(shù)
  Widget buildView();


  //跳轉(zhuǎn)界面
  void push({Widget page, Function popCallback}) {
    print("push: 離開${pageName}界面");
    Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) {
      return page;
    })).then((data) {
      print("pop 進(jìn)入${pageName}界面");
      if (popCallback != null) {
        popCallback(data);
      }
    });
  }

  //路由跳轉(zhuǎn)
  void routerPush({String route, Function popCallback}) {
    print("routerPush: 離開${pageName}界面");
    AppRoutesManager.router.navigateTo(context, route).then((data) {
      print("pop 進(jìn)入${pageName}界面");
      if (popCallback != null) {
        popCallback(data);
      }
    });
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    print("dispose 離開${pageName}界面");
    print("銷毀${pageName}界面");
  }
}

HomePage

_HomePageState繼承自BaseState茬底,以后所有的界面的State都要繼承自BaseState;在initState()函數(shù)里面初始化viewModel获洲,然后使用Provider綁定viewModel阱表。

import 'package:flutter/material.dart';
import 'package:flutter_sg_mvvm/app_manager/app_manager.dart';
import 'package:flutter_sg_mvvm/base/base_state.dart';
import 'package:flutter_sg_mvvm/page/home/model/product_model.dart';
import 'package:flutter_sg_mvvm/page/home/view_model/home_view_model.dart';
import 'package:flutter_sg_mvvm/page/login/login_page.dart';
import 'package:flutter_sg_mvvm/page/shopping_cart/shopping_cart_page.dart';
import 'package:flutter_sg_mvvm/widgets/app_bar/custom_app_bar.dart';
import 'package:flutter_sg_mvvm/widgets/loading_widget/loading_widget.dart';
import 'package:flutter_sg_mvvm/widgets/refresher_footer/refresher_footer.dart';
import 'package:flutter_sg_mvvm/widgets/refresher_header/refresher_header.dart';
import 'package:provider/provider.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';

class HomePage extends StatefulWidget {
  HomePage({Key key}):super(key:key);
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends BaseState<HomePage,HomeViewModel>
    with AutomaticKeepAliveClientMixin {

  @override
  bool get wantKeepAlive => true;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    pageName = "首頁(yè)";
    viewModel = HomeViewModel(context);
    viewModel.refreshData(isShowLoading: true);
  }

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider.value(
      value: viewModel,
      child: Scaffold(
        appBar: CustomAppBar(
          title: pageName,
        ),
        body: initView(),
      ),
    );
  }

  @override
  Widget buildView() {
    return Container(
      child: Column(
        children: <Widget>[
          Expanded(
              child: SmartRefresher(
                enablePullDown: true,
                enablePullUp: true,
                header: RefresherHeader(),
                footer: RefresherFooter(),
                controller: viewModel.refreshController,
                onRefresh: (){
                  viewModel.page = 0;
                  viewModel.refreshData(isShowLoading: false);
                },
                onLoading: (){
                  viewModel.refreshData(isShowLoading: false);
                },
                child: ListView.builder(
                  itemCount: viewModel.dataList.length,
                  itemBuilder: (BuildContext context, index) {
                    ProductModel model = viewModel.dataList[index];
                    return Container(
                      height: 100,
                      child: Column(
                        children: <Widget>[
                          Expanded(
                            child: Row(
                              children: <Widget>[
                                Expanded(
                                  child: Container(
                                    child: Text(model.name),
                                  ),
                                ),
                                GestureDetector(
                                  onTap: (){
                                    viewModel.collectionProduc(index);
                                  },
                                  child: Container(
                                    padding: EdgeInsets.all(10),
                                    child: Text(model.isCollection?'取消':'收藏'),
                                  ),
                                )

                              ],
                            ),
                          ),
                          Expanded(
                            child: Container(
                              child: Row(
                                children: <Widget>[
                                  Expanded(
                                    child: Container(
                                      child: Text('價(jià)格:${model.price}'),
                                    ),
                                  ),
                                  Expanded(
                                    child: Container(
                                      child: Text('數(shù)量:${model.num}'),
                                    ),
                                  ),
                                ],
                              ),
                            ),
                          ),
                        ],
                      ),
                    );
                  },
                ),
              )),
          GestureDetector(
            onTap: () {
              push(page: ShoppingCartPage(), popCallback: (dada) {

              });
            },
            child: Container(
              width: AppManager.width,
              height: 40,
              color: Colors.yellow,
            ),
          )
        ],
      ),
    );

  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
  }
}

mvvm之model:

我將model分為兩層,一層model只是一個(gè)數(shù)據(jù)模型贡珊,一層為services專門從服務(wù)器獲取數(shù)據(jù)最爬。

HomeServices


import 'package:flutter/cupertino.dart';
import 'package:flutter_sg_mvvm/config/api.dart';
import 'package:flutter_sg_mvvm/network/network.dart';
import 'package:flutter_sg_mvvm/page/home/model/product_model.dart';

class HomeServices{

  static Future<dynamic> collectionProduct({
    @required String productId,
    @required int moduleType,
  }) async {
    final res = await Network.post(
      Api.homeList,
      data: {
        'productId': productId,
      },
    );
    return res.data;
  }

  static Future<List<ProductModel>> getHomeList() async {
 //這里使用Future的延時(shí)功能模擬網(wǎng)絡(luò)請(qǐng)求
    List<ProductModel> _dataList = [];
   await Future.delayed(Duration(milliseconds: 2000),(){
      for(int i=0;i<10;i++){
        Map<String,dynamic> data = {
          'name': "產(chǎn)品名字",
          'num': 30,
          'price': 33.5,
          'img': 'HTTP',
        };
        _dataList.add(ProductModel.fromJson(data)) ;
      }
    });
    return _dataList;

//    final res = await Network.get(Api.homeList);
//    return res.data;
  }

}

這里只是我自己做項(xiàng)目的一些經(jīng)驗(yàn),如果有什么寫的不好的地方歡迎大家指正门岔,還有文筆不好爱致,請(qǐng)大家多多包含。這里是是demo的github地址感興趣的朋友可以前往下載寒随。本人QQ:464708476糠悯,大家多多交流。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末妻往,一起剝皮案震驚了整個(gè)濱河市互艾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌讯泣,老刑警劉巖纫普,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異好渠,居然都是意外死亡局嘁,警方通過查閱死者的電腦和手機(jī)溉箕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)悦昵,“玉大人肴茄,你說(shuō)我怎么就攤上這事〉福” “怎么了寡痰?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)棋凳。 經(jīng)常有香客問我拦坠,道長(zhǎng),這世上最難降的妖魔是什么剩岳? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任贞滨,我火速辦了婚禮,結(jié)果婚禮上拍棕,老公的妹妹穿的比我還像新娘晓铆。我一直安慰自己,他們只是感情好绰播,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布骄噪。 她就那樣靜靜地躺著,像睡著了一般蠢箩。 火紅的嫁衣襯著肌膚如雪链蕊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天谬泌,我揣著相機(jī)與錄音滔韵,去河邊找鬼。 笑死掌实,一個(gè)胖子當(dāng)著我的面吹牛陪蜻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播潮峦,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼勇婴!你這毒婦竟也來(lái)了忱嘹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤耕渴,失蹤者是張志新(化名)和其女友劉穎拘悦,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體橱脸,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡础米,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年分苇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屁桑。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡医寿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蘑斧,到底是詐尸還是另有隱情靖秩,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布竖瘾,位于F島的核電站沟突,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏捕传。R本人自食惡果不足惜惠拭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望庸论。 院中可真熱鬧职辅,春花似錦、人聲如沸葡公。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)催什。三九已至涵亏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蒲凶,已是汗流浹背气筋。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留旋圆,地道東北人宠默。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像灵巧,于是被迫代替她去往敵國(guó)和親搀矫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344