【跨平臺開發Flutter】Flutter裏的MVVM

目錄
一、最原始的MVVM
二、使用Listener的MVVM
三、使用Provider的MVVM
四、使用GetX的MVVM

需求很簡單:

  • 搜索框輸入“張”時就請求前綴爲“張”的用戶數據、同時也請求後綴爲“張”的用戶數據,搜索框輸入“李”時就請求前綴爲“李”的用戶數據、同時也請求後綴爲“李”的用戶數據
  • 請求完成後,前綴數據交給上面的ListView顯示,後綴數據交給下面的ListView顯示


一、最原始的MVVM


  • Model層
-----------person_model.dart-----------

/*
 Model的職責:Model只負責封裝數據,不做任何其它操作。
 */

/// Person模型
class PersonModel {
  /// 普通構造方法
  PersonModel({
    this.name,
    this.sex,
    this.age,
  });

  /// 姓名
  String? name;

  /// 性別
  ///
  /// 0-未知,1-男,2-女
  int? sex;

  /// 年齡
  int? age;

  /// 工廠構造方法
  factory PersonModel.fromJson(Map<String, dynamic> json) => PersonModel(
        name: json["name"] == null ? null : json["name"],
        sex: json["sex"] == null ? null : json["sex"],
        age: json["age"] == null ? null : json["age"],
      );

  /// 模型轉字典
  Map<String, dynamic> toJson() => {
        "name": name == null ? null : name,
        "sex": sex == null ? null : sex,
        "age": age == null ? null : age,
      };
}
  • View層
-----------search_bar_widget.dart-----------

/*
 View的職責:View負責響應與業務有關的事件並交給Controller去處理,怎麼交給Controller呢?通過閉包、通知等。
 */

import 'package:flutter/material.dart';

/// 搜索框Widget
class SearchBarWidget extends StatelessWidget {
  SearchBarWidget({
    this.searchTextDidChangeCallback,
  });

  /// 搜索內容改變的回調
  final void Function(String text)? searchTextDidChangeCallback;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.red,
      child: TextField(
        textInputAction: TextInputAction.done,
        onSubmitted: (text) {
          if (searchTextDidChangeCallback != null) {
            searchTextDidChangeCallback!(text);
          }
        },
      ),
    );
  }
}
-----------prefix_list_view_widget.dart-----------

/*
 View的職責:View負責顯示數據,那怎麼顯示數據呢?View可以持有ViewModel。
 */

import 'package:flutter/material.dart';

import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 前綴列表Widget
class PrefixListViewWidget extends StatelessWidget {
  PrefixListViewWidget({
    required this.personViewModel,
  });

  final PersonViewModel personViewModel;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.green,
      child: ListView.builder(
        itemCount: personViewModel.prefixPersonViewModelList.length,
        itemBuilder: (context, index) {
          PersonViewModel personVM =
              personViewModel.prefixPersonViewModelList[index];

          return Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              Text(personVM.name),
              Text(personVM.sex),
              Text("${personVM.age}"),
            ],
          );
        },
        itemExtent: 100,
      ),
    );
  }
}
-----------suffix_list_view_widget.dart-----------

/*
 View的職責:View負責顯示數據,那怎麼顯示數據呢?View可以持有ViewModel。
 */

import 'package:flutter/material.dart';

import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 後綴列表Widget
class SuffixListViewWidget extends StatelessWidget {
  SuffixListViewWidget({
    required this.personViewModel,
  });

  final PersonViewModel personViewModel;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.blue,
      child: ListView.builder(
        itemCount: personViewModel.suffixPersonViewModelList.length,
        itemBuilder: (context, index) {
          PersonViewModel personVM =
              personViewModel.suffixPersonViewModelList[index];

          return Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              Text(personVM.name),
              Text(personVM.sex),
              Text("${personVM.age}"),
            ],
          );
        },
        itemExtent: 100,
      ),
    );
  }
}
  • ViewModel層
-----------base_view_model.dart-----------

import 'package:flutter/cupertino.dart';

enum ViewState {
  loading, // 加載中
  success, // 加載成功
  empty, // 加載成功,但數據爲空
  failure, // 加載失敗
  noNetwork, // 沒網
}

class BaseViewModel extends ChangeNotifier {
  ViewState _state = ViewState.success;

  ViewState get state => _state;

  set state(ViewState value) {
    _state = value;
    notifyListeners();
  }
}
-----------person_view_model.dart-----------

/*
 ViewModel的職責:
  1、ViewModel負責獲取數據;
  2、ViewModel負責處理數據;
  3、ViewModel負責存儲數據。
 */

import 'dart:convert';

import 'package:flutter/services.dart';

import 'package:flutter_mvvm/classes/model/person_model.dart';
import 'package:flutter_mvvm/classes/view_model/base_view_model.dart';

/// Person視圖模型
class PersonViewModel extends BaseViewModel {
  // 命名構造方法,專門用來初始化_personModel
  PersonViewModel._from(PersonModel? personModel) {
    _personModel = personModel;
  }

  // 持有一個_personModel,以便處理數據:ViewModel一對一Model地添加屬性並處理,搞成getter方法即可
  PersonModel? _personModel;

  /// 普通構造方法
  PersonViewModel();

  /// 存儲數據:vm數組
  ///
  /// 真正暴露給外面使用的是vm數組,裏面的數據已經處理好了,直接拿着顯示就行了
  List<PersonViewModel> prefixPersonViewModelList = [];
  List<PersonViewModel> suffixPersonViewModelList = [];

  /// 存儲數據:錯誤消息
  String errorMsg = "";

  /// 處理數據:姓名
  String get name {
    return _personModel?.name ?? "";
  }

  /// 處理數據:性別
  ///
  /// 0-未知,1-男,2-女
  String get sex {
    if (_personModel?.sex == 1) {
      return "男";
    } else if (_personModel?.sex == 2) {
      return "女";
    } else {
      return "未知";
    }
  }

  /// 處理數據:年齡
  int get age {
    return _personModel?.age ?? 0;
  }

  /// 請求前綴數據
  Future<void> loadPrefixData(
      String params, void Function(bool isSuccess) completionHandler) async {
    await Future.delayed(Duration(seconds: 1));

    try {
      String path = "lib/assets/json/${params}_prefix.json";

      String jsonString = await rootBundle.loadString(path);
      List list = jsonDecode(jsonString);

      prefixPersonViewModelList.clear();
      for (Map<String, dynamic> map in list) {
        PersonModel personModel = PersonModel.fromJson(map);
        PersonViewModel personViewModel = PersonViewModel._from(personModel);
        prefixPersonViewModelList.add(personViewModel);
      }

      state = ViewState.success;
      completionHandler(true);
    } catch (error) {
      errorMsg = error.toString();

      state = ViewState.failure;
      completionHandler(false);
    }
  }

  /// 請求後綴數據
  Future<void> loadSuffixData(
      String params, void Function(bool isSuccess) completionHandler) async {
    await Future.delayed(Duration(seconds: 2));

    try {
      String path = "lib/assets/json/${params}_suffix.json";

      String jsonString = await rootBundle.loadString(path);
      List list = jsonDecode(jsonString);

      suffixPersonViewModelList.clear();
      for (Map<String, dynamic> map in list) {
        PersonModel personModel = PersonModel.fromJson(map);
        PersonViewModel personViewModel = PersonViewModel._from(personModel);
        suffixPersonViewModelList.add(personViewModel);
      }

      state = ViewState.success;
      completionHandler(true);
    } catch (error) {
      errorMsg = error.toString();

      state = ViewState.failure;
      completionHandler(false);
    }
  }
}
  • Controller層
-----------list_page.dart-----------

/*
 Controller的職責:
  1、Controller負責持有View,創建View,並把View添加到窗口上顯示;
  2、Controller負責持有ViewModel,調用ViewModel的方法去請求數據;
  3、vm --> view:Controller調用vm的方法請求數據,請求完成後vm是通過回調的方式告訴Controller的:
    請求成功後Controller需要調用一下setState來刷一下UI,這樣view就會去拿vm裏最新存儲的數據來展示了
    請求失敗後Controller可以toast一下錯誤信息給用戶看,或者調用一下setState來刷一下UI,刷成暫無數據那種view
  4、view --> vm:view產生的變化是通過回調告訴Controller的,Controller可以調用vm的方法把view發生的變化告訴它
 */

import 'package:flutter/material.dart';

import 'package:flutter_mvvm/classes/widget/search_bar_widget.dart';
import 'package:flutter_mvvm/classes/widget/prefix_list_view_widget.dart';
import 'package:flutter_mvvm/classes/widget/suffix_list_view_widget.dart';
import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 列表界面
class ListPage extends StatefulWidget {
  const ListPage({Key? key}) : super(key: key);

  @override
  State<ListPage> createState() => _ListPageState();
}

class _ListPageState extends State<ListPage> {
  PersonViewModel _personViewModel = PersonViewModel();

  @override
  Widget build(BuildContext context) {
    print("build");

    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter MVVM"),
      ),
      body: SafeArea(
        child: Column(
          children: [
            _buildSearchBarWidget(),
            Expanded(
              child: _buildPrefixListViewWidget(),
            ),
            Expanded(
              child: _buildSuffixListViewWidget(),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSearchBarWidget() {
    print("_buildSearchBarWidget");

    return Container(
      color: Colors.red,
      child: SearchBarWidget(
        searchTextDidChangeCallback: (text) {
          _personViewModel.loadPrefixData(text, (isSuccess) {
            if (mounted) {
              // 注意要判斷一下當前界面還在不在視圖樹中,因爲請求都是異步的,很有可能請求還沒完成我們就退出界面了,退出界面時我們什麼都不做即可
              if (isSuccess) {
                setState(() {});
              } else {
                print(_personViewModel.errorMsg);
              }
            }
          });

          _personViewModel.loadSuffixData(text, (isSuccess) {
            if (mounted) {
              // 注意要判斷一下當前界面還在不在視圖樹中,因爲請求都是異步的,很有可能請求還沒完成我們就退出界面了,退出界面時我們什麼都不做即可
              if (isSuccess) {
                setState(() {});
              } else {
                print(_personViewModel.errorMsg);
              }
            }
          });
        },
      ),
    );
  }

  Widget _buildPrefixListViewWidget() {
    print("_buildPrefixListViewWidget");

    return PrefixListViewWidget(
      personViewModel: _personViewModel,
    );
  }

  Widget _buildSuffixListViewWidget() {
    print("_buildPrefixListViewWidget");

    return SuffixListViewWidget(
      personViewModel: _personViewModel,
    );
  }
}


二、使用Listener的MVVM


相對於最原始的MVVM來說,它的變化其實就是【Controller調用vm的方法請求數據,請求完成後vm通過什麼方式告訴Controller,之前是通過回調的方式,現在是通過Listener的方式】。

它的優勢就是【界面退出時假如我們的請求還沒走完,會自動移除監聽】,也就是說就算網絡請求完了也不會再觸發Controller裏的監聽了,我們壓根兒不需要關心這個界面在不在視圖樹裏,只需要做業務即可。【它的劣勢就是因爲所有的業務都會觸發同一個監聽而使得代碼判斷起來複雜了,之前起碼是一個業務一個回調、各管各的】,它給人感覺上好像是在數據驅動UI,但其實並不是,因爲這裏並不存在數據和UI的綁定操作,本質上還是數據變化後、我們手動刷新UI來展示最新的數據。

因此相對於最原始的MVVM來說,更推薦最原始的MVVM。

  • Model層和View層都不需要改動
  • ViewModel層
-----------base_view_model.dart-----------

import 'package:flutter/cupertino.dart';

enum ViewState {
  loading, // 加載中
  success, // 加載成功
  empty, // 加載成功,但數據爲空
  failure, // 加載失敗
  noNetwork, // 沒網
}

class BaseViewModel extends ChangeNotifier {
  ViewState _state = ViewState.success;

  ViewState get state => _state;

  set state(ViewState value) {
    _state = value;
    notifyListeners();
  }
}
-----------person_view_model.dart-----------

/*
 ViewModel的職責:
  1、ViewModel負責獲取數據;
  2、ViewModel負責處理數據;
  3、ViewModel負責存儲數據。
 */

import 'dart:convert';

import 'package:flutter/services.dart';

import 'package:flutter_mvvm/classes/model/person_model.dart';
import 'package:flutter_mvvm/classes/view_model/base_view_model.dart';

/// PersonViewModel裏所做的業務
class PersonViewModelService {
  static const String loadPrefixData = "loadPrefixData";
  static const String loadSuffixData = "loadSuffixData";
}

/// PersonViewModel裏所做業務的結果
class PersonViewModelServiceResult {
  PersonViewModelServiceResult({
    required this.service,
    required this.isSuccess,
  });

  /// 具體是哪個業務
  String service;

  /// 結果
  bool isSuccess;
}

/// Person視圖模型
///
/// 要想使用Listener,PersonViewModel得繼承自或者混入ChangeNotifier
class PersonViewModel extends BaseViewModel {
  late PersonViewModelServiceResult serviceResult;

  // 命名構造方法,專門用來初始化_personModel
  PersonViewModel._from(PersonModel? personModel) {
    _personModel = personModel;
  }

  // 持有一個_personModel,以便處理數據:ViewModel一對一Model地添加屬性並處理,搞成getter方法即可
  PersonModel? _personModel;

  /// 普通構造方法
  PersonViewModel();

  /// 存儲數據:vm數組
  ///
  /// 真正暴露給外面使用的是vm數組,裏面的數據已經處理好了,直接拿着顯示就行了
  List<PersonViewModel> prefixPersonViewModelList = [];
  List<PersonViewModel> suffixPersonViewModelList = [];

  /// 存儲數據:錯誤消息
  String errorMsg = "";

  /// 處理數據:姓名
  String get name {
    return _personModel?.name ?? "";
  }

  /// 處理數據:性別
  ///
  /// 0-未知,1-男,2-女
  String get sex {
    if (_personModel?.sex == 1) {
      return "男";
    } else if (_personModel?.sex == 2) {
      return "女";
    } else {
      return "未知";
    }
  }

  /// 處理數據:年齡
  int get age {
    return _personModel?.age ?? 0;
  }

  /// 請求前綴數據
  ///
  /// 請求完成後,本來是通過回調告訴Controller的,現在不要回調了,通過Listener告訴Controller
  Future<void> loadPrefixData(String params) async {
    await Future.delayed(Duration(seconds: 1));

    try {
      String path = "lib/assets/json/${params}_prefix.json";

      String jsonString = await rootBundle.loadString(path);
      List list = jsonDecode(jsonString);

      prefixPersonViewModelList.clear();
      for (Map<String, dynamic> map in list) {
        PersonModel personModel = PersonModel.fromJson(map);
        PersonViewModel personViewModel = PersonViewModel._from(personModel);
        prefixPersonViewModelList.add(personViewModel);
      }

      state = ViewState.success;
      serviceResult = PersonViewModelServiceResult(
          service: PersonViewModelService.loadPrefixData, isSuccess: true);
      notifyListeners();
    } catch (error) {
      errorMsg = error.toString();

      state = ViewState.failure;
      serviceResult = PersonViewModelServiceResult(
          service: PersonViewModelService.loadPrefixData, isSuccess: false);
      notifyListeners();
    }
  }

  /// 請求後綴數據
  ///
  /// 請求完成後,本來是通過回調告訴Controller的,現在不要回調了,通過Listener告訴Controller
  Future<void> loadSuffixData(String params) async {
    await Future.delayed(Duration(seconds: 2));

    try {
      String path = "lib/assets/json/${params}_suffix.json";

      String jsonString = await rootBundle.loadString(path);
      List list = jsonDecode(jsonString);

      suffixPersonViewModelList.clear();
      for (Map<String, dynamic> map in list) {
        PersonModel personModel = PersonModel.fromJson(map);
        PersonViewModel personViewModel = PersonViewModel._from(personModel);
        suffixPersonViewModelList.add(personViewModel);
      }

      state = ViewState.success;
      serviceResult = PersonViewModelServiceResult(
          service: PersonViewModelService.loadSuffixData, isSuccess: true);
      notifyListeners();
    } catch (error) {
      errorMsg = error.toString();

      state = ViewState.failure;
      serviceResult = PersonViewModelServiceResult(
          service: PersonViewModelService.loadSuffixData, isSuccess: false);
      notifyListeners();
    }
  }
}
  • Controller層
-----------list_page.dart-----------

/*
 Controller的職責:
  1、Controller負責持有View,創建View,並把View添加到窗口上顯示;
  2、Controller負責持有ViewModel,調用ViewModel的方法去請求數據;
  3、vm --> view:Controller調用vm的方法請求數據,請求完成後vm是通過Listener而非回調的方式告訴Controller的:
    請求成功後Controller需要調用一下setState來刷一下UI,這樣view就會去拿vm裏最新存儲的數據來展示了
    請求失敗後Controller可以toast一下錯誤信息給用戶看,或者調用一下setState來刷一下UI,刷成暫無數據那種view
  4、view --> vm:view產生的變化是通過回調告訴Controller的,Controller可以調用vm的方法把view發生的變化告訴它
 */

import 'package:flutter/material.dart';

import 'package:flutter_mvvm/classes/widget/search_bar_widget.dart';
import 'package:flutter_mvvm/classes/widget/prefix_list_view_widget.dart';
import 'package:flutter_mvvm/classes/widget/suffix_list_view_widget.dart';
import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 列表界面
class ListPage extends StatefulWidget {
  const ListPage({Key? key}) : super(key: key);

  @override
  State<ListPage> createState() => _ListPageState();
}

class _ListPageState extends State<ListPage> {
  PersonViewModel _personViewModel = PersonViewModel();

  @override
  void initState() {
    // 第一步:_personViewModel添加監聽
    //
    // 它裏面任何時候、任何地方發出notifyListeners,都會觸發這裏添加好的監聽
    // 那它裏面什麼時候、什麼地方發出notifyListeners呢?當然就是請求完成的時候,在請求完成的回調裏發出
    // 當然因爲它內部可能會做多個業務,如果多個業務都發出了notifyListeners,則都會觸發這裏的同一個回調,因此我們會在它裏面添加一個類來區分到底是哪個業務完成了,以便在監聽裏處理不同的業務
    _personViewModel.addListener(_personViewModelListener);

    super.initState();
  }

  // 第二步:_personViewModel處理監聽
  void _personViewModelListener() {
    if (_personViewModel.serviceResult.service ==
        PersonViewModelService.loadPrefixData) {
      if (_personViewModel.serviceResult.isSuccess) {
        setState(() {});
      } else {
        print(_personViewModel.errorMsg);
      }
    } else if (_personViewModel.serviceResult.service ==
        PersonViewModelService.loadSuffixData) {
      if (_personViewModel.serviceResult.isSuccess) {
        setState(() {});
      } else {
        print(_personViewModel.errorMsg);
      }
    }
  }

  @override
  void dispose() {
    // 第三步:_personViewModel移除監聽
    _personViewModel.removeListener(_personViewModelListener);

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    print("build");

    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter MVVM"),
      ),
      body: SafeArea(
        child: Column(
          children: [
            _buildSearchBarWidget(),
            Expanded(
              child: _buildPrefixListViewWidget(),
            ),
            Expanded(
              child: _buildSuffixListViewWidget(),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSearchBarWidget() {
    print("_buildSearchBarWidget");

    return SearchBarWidget(
      searchTextDidChangeCallback: (text) {
        _personViewModel.loadPrefixData(text);
        _personViewModel.loadSuffixData(text);
      },
    );
  }

  Widget _buildPrefixListViewWidget() {
    print("_buildPrefixListViewWidget");

    return PrefixListViewWidget(
      personViewModel: _personViewModel,
    );
  }

  Widget _buildSuffixListViewWidget() {
    print("_buildSuffixListViewWidget");

    return SuffixListViewWidget(
      personViewModel: _personViewModel,
    );
  }
}


三、使用Provider的MVVM


首先我們要明白Provider這個框架的首要功能是數據共享,所以我們這裏使用Provider的首要目的就是用它來共享_personViewModel這個數據,因爲我們的ListPage、PrefixListViewWidget和SuffixListViewWidget都使用到了同一個_personViewModel,當然我們的例子可能比較簡單,【實際開發中你的一個界面裏可能會由很多很多個Widget組成,而它們可能都需要使用同一個viewModel,甚至多個界面之間也需要使用同一個viewModel,那最原始的寫法就是像最原始的MVVM裏那樣通過指針傳遞viewModel來共享,而Provider則提供了另外一種共享數據的方式——只要一堆Widget擁有同一個Provider作爲父視圖,那麼這些Widget就都可以共享這個Provider綁定的viewModel數據】,這就是它相對於最原始MVVM的第一個變化,這個變化主要解決了viewModel傳來傳去的問題。(如果你只想一個界面內的多個Widget共享數據,那麼這多個Widget的父視圖就必須得是這個Provider,所以你可以把Provider包在這個界面上即可;如果你想多個界面之間共享數據,那麼這多個界面的父視圖就必須得是這個Provider,因此這個時候我們會把Provider包在App的最底層)

實現了上面的內容之後其實已經完成了Provider數據共享的功能,【但是我們會發現使用Provider時,它要求我們的ViewModel必須繼承自或混入ChangeNotifier,這是因爲Provider的第二個功能就是局部刷新,也就是說我們只需要在ViewModel裏合適的時機、合適的地方發出一個notifyListeners,Provider就會自動觸發它Consumer或Selector的回調來只刷新局部UI來展示最新的數據、當然我們也可以在些回調裏Toast錯誤消息等。同時我們也只需要在ViewModel裏發notifyListeners就行了,也不用像使用Listener那樣考慮到底是什麼業務完成了而做一堆判斷,Controller裏也沒有什麼監聽,因爲各個監聽已經分散到了各個Consumer或Selector回調那裏】,這就是它相對於最原始MVVM的第二個變化,這個變化主要解決了ViewModel怎麼把變化告訴Controller的問題。

【它的優勢就是更加簡單的數據共享方式 + 響應式編程數據驅動UI。】【它的劣勢就是Provider框架的侵入性太強了,而且代碼編寫起來有點費勁。】

因此相對於最原始的MVVM來說,你有餘力的話可以學學Provider並應用在你的MVVM中。

  • App最底層
-----------main.dart-----------

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'package:flutter_mvvm/classes/page/list_page.dart';
import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

void main() {
  // 第一步:創建數據存放地,也就是我們想共享的view_model———即person_view_model.dart
  // 第二步:在App的最底層外再包一層ChangeNotifierProvider,並把我們需要共享的view_model傳給ChangeNotifierProvider的create屬性
  runApp(ChangeNotifierProvider(
    create: (ctx) => PersonViewModel(),
    child: MyApp(),
  ));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ListPage(),
    );
  }
}
  • Model層不需要改動
  • ViewModel層
-----------base_view_model.dart-----------

import 'package:flutter/cupertino.dart';

enum ViewState {
  loading, // 加載中
  success, // 加載成功
  empty, // 加載成功,但數據爲空
  failure, // 加載失敗
  noNetwork, // 沒網
}

class BaseViewModel extends ChangeNotifier {
  ViewState _state = ViewState.success;

  ViewState get state => _state;

  set state(ViewState value) {
    _state = value;
    notifyListeners();
  }
}
-----------person_view_model.dart-----------

/*
 ViewModel的職責:
  1、ViewModel負責獲取數據;
  2、ViewModel負責處理數據;
  3、ViewModel負責存儲數據。
 */

import 'dart:convert';

import 'package:flutter/services.dart';

import 'package:flutter_mvvm/classes/model/person_model.dart';
import 'package:flutter_mvvm/classes/view_model/base_view_model.dart';

/// Person視圖模型
///
/// 【變化一】:要想使用Provider,PersonViewModel得繼承自或者混入ChangeNotifier
class PersonViewModel extends BaseViewModel {
  // 命名構造方法,專門用來初始化_personModel
  PersonViewModel._from(PersonModel? personModel) {
    _personModel = personModel;
  }

  // 持有一個_personModel,以便處理數據:ViewModel一對一Model地添加屬性並處理,搞成getter方法即可
  PersonModel? _personModel;

  /// 普通構造方法
  PersonViewModel();

  /// 存儲數據:vm數組
  ///
  /// 真正暴露給外面使用的是vm數組,裏面的數據已經處理好了,直接拿着顯示就行了
  List<PersonViewModel> prefixPersonViewModelList = [];
  List<PersonViewModel> suffixPersonViewModelList = [];

  /// 存儲數據:錯誤消息
  String errorMsg = "";

  /// 處理數據:姓名
  String get name {
    return _personModel?.name ?? "";
  }

  /// 處理數據:性別
  ///
  /// 0-未知,1-男,2-女
  String get sex {
    if (_personModel?.sex == 1) {
      return "男";
    } else if (_personModel?.sex == 2) {
      return "女";
    } else {
      return "未知";
    }
  }

  /// 處理數據:年齡
  int get age {
    return _personModel?.age ?? 0;
  }

  /// 請求前綴數據
  Future<void> loadPrefixData(String params) async {
    await Future.delayed(Duration(seconds: 1));

    try {
      String path = "lib/assets/json/${params}_prefix.json";

      String jsonString = await rootBundle.loadString(path);
      List list = jsonDecode(jsonString);

      prefixPersonViewModelList.clear();
      for (Map<String, dynamic> map in list) {
        PersonModel personModel = PersonModel.fromJson(map);
        PersonViewModel personViewModel = PersonViewModel._from(personModel);
        prefixPersonViewModelList.add(personViewModel);
      }

      //【變化二】
      state = ViewState.success;
    } catch (error) {
      errorMsg = error.toString();

      //【變化二】
      state = ViewState.failure;
    }
  }

  /// 請求後綴數據
  Future<void> loadSuffixData(String params) async {
    await Future.delayed(Duration(seconds: 2));

    try {
      String path = "lib/assets/json/${params}_suffix.json";

      String jsonString = await rootBundle.loadString(path);
      List list = jsonDecode(jsonString);

      suffixPersonViewModelList.clear();
      for (Map<String, dynamic> map in list) {
        PersonModel personModel = PersonModel.fromJson(map);
        PersonViewModel personViewModel = PersonViewModel._from(personModel);
        suffixPersonViewModelList.add(personViewModel);
      }

      //【變化二】
      state = ViewState.success;
    } catch (error) {
      errorMsg = error.toString();

      //【變化二】
      state = ViewState.failure;
    }
  }
}
  • View層
-----------prefix_list_view_widget.dart-----------

/*
 View的職責:View負責顯示數據,那怎麼顯示數據呢?View可以持有ViewModel。
 */

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 前綴列表Widget
class PrefixListViewWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("【PrefixListViewWidget】---build");

    return Container(
      color: Colors.green,
      // 第三步(get的情況):在需要使用共享數據的Widget外包一層Consumer或者Selector來使用共享數據,需要get共享數據的地方使用Consumer
      // Consumer是個泛型,泛的型就是問要使用哪個viewModel裏的數據,填進去即可
      child: Consumer<PersonViewModel>(
        // Consumer有一個必添屬性builder,接收一個函數作爲屬性值,該函數的第二個參數就是共享數據存放地
        // 當數據發生變化時,就會觸發這個builder函數來刷新Widget,所以我們在這個函數裏返回原始的Widget,從而達到Widget包裹Consumer的目的
        builder: (context, personViewModel, child) {
          print("【PrefixListViewWidget】------Consumer");

          return ListView.builder(
            itemCount: personViewModel.prefixPersonViewModelList.length,
            itemBuilder: (context, index) {
              PersonViewModel personVM =
                  personViewModel.prefixPersonViewModelList[index];

              return Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  Text(personVM.name),
                  Text(personVM.sex),
                  Text("${personVM.age}"),
                ],
              );
            },
            itemExtent: 100,
          );
        },
      ),
    );
  }
}
-----------suffix_list_view_widget.dart-----------

/*
 View的職責:View負責顯示數據,那怎麼顯示數據呢?View可以持有ViewModel。
 */

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 後綴列表Widget
class SuffixListViewWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("【PrefixListViewWidget】---build");

    return Container(
      color: Colors.blue,
      // 第三步(get的情況):在需要使用共享數據的Widget外包一層Consumer或者Selector來使用共享數據,需要get共享數據的地方使用Consumer
      // Consumer是個泛型,泛的型就是問要使用哪個viewModel裏的數據,填進去即可
      child: Consumer<PersonViewModel>(
        // Consumer有一個必添屬性builder,接收一個函數作爲屬性值,該函數的第二個參數就是共享數據存放地
        // 當數據發生變化時,就會觸發這個builder函數來刷新Widget,所以我們在這個函數裏返回原始的Widget,從而達到Widget包裹Consumer的目的
        builder: (context, personViewModel, child) {
          print("【SuffixListViewWidget】------Consumer");

          return ListView.builder(
            itemCount: personViewModel.suffixPersonViewModelList.length,
            itemBuilder: (context, index) {
              PersonViewModel personVM =
                  personViewModel.suffixPersonViewModelList[index];

              return Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  Text(personVM.name),
                  Text(personVM.sex),
                  Text("${personVM.age}"),
                ],
              );
            },
            itemExtent: 100,
          );
        },
      ),
    );
  }
}
  • Controller層
-----------list_page.dart-----------

/*
 Controller的職責:
  1、Controller負責持有View,創建View,並把View添加到窗口上顯示;
  2、Controller負責持有ViewModel,調用ViewModel的方法去請求數據;
  3、vm --> view:Controller調用vm的方法請求數據,請求完成後vm是通過回調的方式告訴Controller的:
    請求成功後Controller需要調用一下setState來刷一下UI,這樣view就會去拿vm裏最新存儲的數據來展示了
    請求失敗後Controller可以toast一下錯誤信息給用戶看,或者調用一下setState來刷一下UI,刷成暫無數據那種view
  4、view --> vm:view產生的變化是通過回調告訴Controller的,Controller可以調用vm的方法把view發生的變化告訴它
 */

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:fluttertoast/fluttertoast.dart';

import 'package:flutter_mvvm/classes/widget/search_bar_widget.dart';
import 'package:flutter_mvvm/classes/widget/prefix_list_view_widget.dart';
import 'package:flutter_mvvm/classes/widget/suffix_list_view_widget.dart';
import 'package:flutter_mvvm/classes/view_model/base_view_model.dart';
import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 列表界面
class ListPage extends StatefulWidget {
  const ListPage({Key? key}) : super(key: key);

  @override
  State<ListPage> createState() => _ListPageState();
}

class _ListPageState extends State<ListPage> {
  @override
  Widget build(BuildContext context) {
    print("build");

    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter MVVM"),
      ),
      body: SafeArea(
        child: Column(
          children: [
            _buildSearchBarWidget(),
            Expanded(
              child: _buildTwoListViewWidget(),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSearchBarWidget() {
    print("_buildSearchBarWidget");

    // 第三步(set的情況):在需要使用共享數據的Widget外包一層Consumer或者Selector來使用共享數據,需要set共享數據的地方使用Selector
    // Selector是個泛型,而且還泛了兩個型,第一個泛的型是問要使用哪個viewModel裏的數據,填進去即可,第二個泛的型是轉換之後的數據類型,比如我這裏轉換之後依然是使用PersonViewModel,那麼我們填一樣的類型即可(大多數情況都不需要轉吧)
    return Selector<PersonViewModel, PersonViewModel>(
      // Selector有一個必添屬性selector,接收一個函數作爲屬性值,該函數的第二個參數就是共享數據存放地
      // 該函數用來指定Selector的兩個泛型之間如何進行數據轉換,這裏我們不轉換,所以返回原先的personViewModel
      // 如果轉換後,則下面builder函數真正渲染Widget時的第二個參數就是轉換後的ViewModel了,不轉換的話builder函數的第二個參數就是原始的ViewModel
      selector: (ctx, personViewModel) {
        return personViewModel;
      },
      // Selector有一個必添屬性builder,接收一個函數作爲屬性值,該函數的第二個參數就是共享數據存放地
      // 當數據發生變化時,就會觸發這個builder函數來刷新Widget,所以我們在這個函數裏返回原始的Widget,從而達到Widget包裹Selector的目的
      builder: (context, personViewModel, child) {
        print("_buildSearchBarWidget---Selector");

        return Container(
          color: Colors.red,
          child: SearchBarWidget(
            searchTextDidChangeCallback: (text) {
              personViewModel.loadPrefixData(text);
              personViewModel.loadSuffixData(text);
            },
          ),
        );
      },
      // 當共享數據發生變化時,是否執行builder方法重新構建Widget,我們可以設定爲 return pre = next,不一樣時才刷新,一樣就不刷新
      // 但是因爲本次案例裏是僅僅set共享數據,不需要get變化後的共享數據來刷新Widget的情況,所以這種情況我們總是返回false就ok
      shouldRebuild: (prev, next) {
        return false;
      },
    );
  }

  Widget _buildTwoListViewWidget() {
    return Consumer<PersonViewModel>(
        builder: (context, personViewModel, child) {
      switch (personViewModel.state) {
        case ViewState.success:
          return Column(
            children: [
              Expanded(
                child: _buildPrefixListViewWidget(),
              ),
              Expanded(
                child: _buildSuffixListViewWidget(),
              ),
            ],
          );
        default:
          Fluttertoast.showToast(
            msg: personViewModel.errorMsg,
            gravity: ToastGravity.CENTER,
          );

          return Center(
            child: Text("${personViewModel.errorMsg}"),
          );
      }
    });
  }

  Widget _buildPrefixListViewWidget() {
    print("_buildPrefixListViewWidget");

    return PrefixListViewWidget();
  }

  Widget _buildSuffixListViewWidget() {
    print("_buildPrefixListViewWidget");

    return SuffixListViewWidget();
  }
}


四、使用GetX的MVVM


GetX支持響應式編程,使用它可以非常簡單地實現數據驅動UI的效果,只需要兩步:一在ViewModel裏把外界想監聽的數據通過.obs搞成Observable,二在外界想使用數據的地方通過Obx(() => Widget)包裹真正的Widget搞成Observer,這樣就可以數據驅動UI了,這就是它相對於最原始MVVM的第一個變化,這個變化主要解決了ViewModel怎麼把變化告訴Controller的問題。

GetX也支持數據共享,也只需要兩步:一在Controller裏本來創建_viewModel的地方Get.put一下,二完事就可以在任何想使用_viewModel的地方Get.find到它來使用了,這就是它相對於最原始MVVM的第二個變化,這個變化主要解決了viewModel傳來傳去的問題。

【它的優勢就是非常簡單地響應式編程數據驅動UI + 非常簡單得數據共享方式,比RxDart簡單地多。】【它的劣勢就是GetX框架的侵入性太強了。】

因此相對於最原始的MVVM來說,你有餘力的話可以學學GetX並應用在你的MVVM中;相對於使用Provider的MVVM來說,則強烈推薦使用GetX,它的使用簡直太簡單了。

當然GetX還提供了很多其它的功能,如相對於系統自帶的Navigator更加簡單地路由管理App國際化等,可以根據自己的情況選擇使用。

  • Model層不需要改動
  • ViewModel層
-----------base_view_model.dart-----------

import 'package:get/get.dart';

enum ViewState {
  loading, // 加載中
  success, // 加載成功
  empty, // 加載成功,但數據爲空
  failure, // 加載失敗
  noNetwork, // 沒網
}

class BaseViewModel extends GetxController {
  Rx<ViewState> state = ViewState.success.obs;
}
-----------person_view_model.dart-----------

/*
 ViewModel的職責:
  1、ViewModel負責獲取數據;
  2、ViewModel負責處理數據;
  3、ViewModel負責存儲數據。
 */

import 'dart:convert';

import 'package:flutter/services.dart';

import 'package:flutter_mvvm/classes/model/person_model.dart';
import 'package:flutter_mvvm/classes/view_model/base_view_model.dart';

/// Person視圖模型
class PersonViewModel extends BaseViewModel {
  // 命名構造方法,專門用來初始化_personModel
  PersonViewModel._from(PersonModel? personModel) {
    _personModel = personModel;
  }

  // 持有一個_personModel,以便處理數據:ViewModel一對一Model地添加屬性並處理,搞成getter方法即可
  PersonModel? _personModel;

  /// 普通構造方法
  PersonViewModel();

  /// 存儲數據:vm數組
  ///
  /// 真正暴露給外面使用的是vm數組,裏面的數據已經處理好了,直接拿着顯示就行了
  List<PersonViewModel> prefixPersonViewModelList = [];
  List<PersonViewModel> suffixPersonViewModelList = [];

  /// 存儲數據:錯誤消息
  String errorMsg = "";

  /// 處理數據:姓名
  String get name {
    return _personModel?.name ?? "";
  }

  /// 處理數據:性別
  ///
  /// 0-未知,1-男,2-女
  String get sex {
    if (_personModel?.sex == 1) {
      return "男";
    } else if (_personModel?.sex == 2) {
      return "女";
    } else {
      return "未知";
    }
  }

  /// 處理數據:年齡
  int get age {
    return _personModel?.age ?? 0;
  }

  /// 請求前綴數據
  Future<void> loadPrefixData(String params) async {
    await Future.delayed(Duration(seconds: 1));

    try {
      String path = "lib/assets/json/${params}_prefix.json";

      String jsonString = await rootBundle.loadString(path);
      List list = jsonDecode(jsonString);

      prefixPersonViewModelList.clear();
      for (Map<String, dynamic> map in list) {
        PersonModel personModel = PersonModel.fromJson(map);
        PersonViewModel personViewModel = PersonViewModel._from(personModel);
        prefixPersonViewModelList.add(personViewModel);
      }

      loadSuffixData(params);
    } catch (error) {
      errorMsg = error.toString();

      state.value = ViewState.failure;
    }
  }

  /// 請求後綴數據
  Future<void> loadSuffixData(String params) async {
    await Future.delayed(Duration(seconds: 2));

    try {
      String path = "lib/assets/json/${params}_suffix.json";

      String jsonString = await rootBundle.loadString(path);
      List list = jsonDecode(jsonString);

      suffixPersonViewModelList.clear();
      for (Map<String, dynamic> map in list) {
        PersonModel personModel = PersonModel.fromJson(map);
        PersonViewModel personViewModel = PersonViewModel._from(personModel);
        suffixPersonViewModelList.add(personViewModel);
      }

      state.value = ViewState.success;
    } catch (error) {
      errorMsg = error.toString();

      state.value = ViewState.failure;
    }
  }
}
  • View層
-----------prefix_list_view_widget.dart-----------

/*
 View的職責:View負責顯示數據,那怎麼顯示數據呢?View可以持有ViewModel。
 */

import 'package:flutter/material.dart';
import 'package:get/get.dart';

import 'package:flutter_mvvm/classes/view_model/base_view_model.dart';
import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 前綴列表Widget
class PrefixListViewWidget extends StatelessWidget {
  final _personViewModel = Get.find<PersonViewModel>();

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.green,
      child: Obx(
        () {
          print("---PrefixListViewWidget---");

          if (_personViewModel.state.value == ViewState.failure) {
            print(_personViewModel.errorMsg);

            return Center(
              child: Text("${_personViewModel.errorMsg})"),
            );
          } else {
            return ListView.builder(
              itemCount: _personViewModel.prefixPersonViewModelList.length,
              itemBuilder: (context, index) {
                PersonViewModel personVM =
                _personViewModel.prefixPersonViewModelList[index];

                return Row(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    Text(personVM.name),
                    Text(personVM.sex),
                    Text("${personVM.age}"),
                  ],
                );
              },
              itemExtent: 100,
            );
          }
        },
      ),
    );
  }
}
-----------suffix_list_view_widget.dart-----------

/*
 View的職責:View負責顯示數據,那怎麼顯示數據呢?View可以持有ViewModel。
 */

import 'package:flutter/material.dart';
import 'package:get/get.dart';

import 'package:flutter_mvvm/classes/view_model/base_view_model.dart';
import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 後綴列表Widget
class SuffixListViewWidget extends StatelessWidget {
  final _personViewModel = Get.find<PersonViewModel>();

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.blue,
      child: Obx(() {
        print("---SuffixListViewWidget---");

        if (_personViewModel.state.value == ViewState.failure) {
          print(_personViewModel.errorMsg);

          return Center(
            child: Text("${_personViewModel.errorMsg})"),
          );
        } else {
          return ListView.builder(
            itemCount: _personViewModel.suffixPersonViewModelList.length,
            itemBuilder: (context, index) {
              PersonViewModel personVM =
                  _personViewModel.suffixPersonViewModelList[index];

              return Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  Text(personVM.name),
                  Text(personVM.sex),
                  Text("${personVM.age}"),
                ],
              );
            },
            itemExtent: 100,
          );
        }
      }),
    );
  }
}
  • Controller層
-----------list_page.dart-----------

/*
 Controller的職責:
  1、Controller負責持有View,創建View,並把View添加到窗口上顯示;
  2、Controller負責持有ViewModel,調用ViewModel的方法去請求數據;
  3、vm --> view:Controller調用vm的方法請求數據,請求完成後vm是通過回調的方式告訴Controller的:
    請求成功後Controller需要調用一下setState來刷一下UI,這樣view就會去拿vm裏最新存儲的數據來展示了
    請求失敗後Controller可以toast一下錯誤信息給用戶看,或者調用一下setState來刷一下UI,刷成暫無數據那種view
  4、view --> vm:view產生的變化是通過回調告訴Controller的,Controller可以調用vm的方法把view發生的變化告訴它
 */

import 'package:flutter/material.dart';
import 'package:get/get.dart';

import 'package:flutter_mvvm/classes/widget/search_bar_widget.dart';
import 'package:flutter_mvvm/classes/widget/prefix_list_view_widget.dart';
import 'package:flutter_mvvm/classes/widget/suffix_list_view_widget.dart';
import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 列表界面
class ListPage extends StatelessWidget {
  final _personViewModel = Get.put(PersonViewModel());

  @override
  Widget build(BuildContext context) {
    print("build");

    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter MVVM"),
      ),
      body: SafeArea(
        child: Column(
          children: [
            _buildSearchBarWidget(),
            Expanded(
              child: _buildPrefixListViewWidget(),
            ),
            Expanded(
              child: _buildSuffixListViewWidget(),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSearchBarWidget() {
    print("_buildSearchBarWidget");

    return Container(
      color: Colors.red,
      child: SearchBarWidget(
        searchTextDidChangeCallback: (text) {
          _personViewModel.loadPrefixData(text);
        },
      ),
    );
  }

  Widget _buildPrefixListViewWidget() {
    print("_buildPrefixListViewWidget");

    return PrefixListViewWidget();
  }

  Widget _buildSuffixListViewWidget() {
    print("_buildPrefixListViewWidget");

    return SuffixListViewWidget();
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章