Flutter 中的 MVVM 實現

android大哥發表於2021-08-06

概述

其實本身 flutter 就是一個 mvvm 的架構, 當你載入完資料 ,直接 setState() 就可以了,

MVVM各代表什麼

  • M:Model class,便是指這裡的Repository ,主要負責從本地資料庫或者遠端伺服器來獲取資料,Repository統一了資料的入口,獲取到資料,將資料傳送給 ViewModel
  • VM:ViewModel ,即 Flutter 中的ValueNotifier 或者是 ChangeNotifier
  • V:View ,即 Flutter 中 Widget,也可以認為 ValueListenableBuilder 或者是 ChangeNotifierProvider

使用MVVM實現一個簡單的登入頁面

比如我們去請求一個網路的書籍

先建 Model

class BookModel {
 
  String? name; // 書的名字
  String? author;// 書的作者
  String? detail;

  int loadState = 1; // 0 : 載入中  1:載入成功 2:載入失敗
}
複製程式碼

建立我們的BookRepository

下面的是模擬網路去請求資料,用一個變數 當 count 是偶數的時候模擬網路錯誤,是奇數的時候模擬網路成功

class BookRepository {
  var count = 0;
  Future<BookModel> getData() async {
    await Future.delayed(const Duration(seconds: 2));
    BookModel model = BookModel();
    count++;
    if (count.isOdd) {
      throw Exception("網路錯誤");
    } else {
      model.name = "Flutter$count";
      model.author = "google$count";
    }

    return model;
  }
}
複製程式碼

再寫ViewModel層

class BookViewModel extends ValueNotifier<BookModel> {
 // 建立 BookRepository
  BookRepository repository = BookRepository();

  BookViewModel() : super(BookModel());
  
  // 當 已經 dispose 的時候,就不要在發了
  bool _dispose = false;

  @override
  void dispose() {
    super.dispose();
    _dispose = true;
  }

  @override
  void notifyListeners() {
    if (!_dispose) {
      super.notifyListeners();
    }
  }

 /// 用來請求資料
  void getData() {
   // 這裡的value 就是 BookModel
   // 設定為載入中
    value.loadState = 0;
    // 通知改變
    notifyListeners();
    repository.getData().then((book) {
     // 設定載入成功
      book.loadState = 1;
      // 賦值的時候 會呼叫notifyListeners
      value = book;
    }).catchError((e) {
     // 設定網路錯誤的碼
      value.loadState = 2;
      notifyListeners();
    });
  }
}

複製程式碼

建立我們的View層

下面有使用 ValueListenableBuilder 以及 ChangeNotifierProvider 兩種方式去實現,具體請看程式碼

class FlutterBook extends StatefulWidget {
  FlutterBook({Key? key}) : super(key: key);

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

class _FlutterBookState extends State<FlutterBook> {
  final BookViewModel _viewModel = BookViewModel();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('MVVM的demo'),
      ),
      // body: _useValueListenableBuilderBody(),
      body: _useProviderBody(),
    );
  }

  /// 使用 ValueListenableBuilder
  Widget _useValueListenableBuilder() {
    return ValueListenableBuilder<BookModel>(
      valueListenable: _viewModel,
      builder: (BuildContext context, BookModel model, Widget? child) {
        return _bodyChild(model);
      },
    );
  }
  /// 使用 ChangeNotifierProvider
  Widget _useProviderBody() {
    return ChangeNotifierProvider(
      create: (_) => _viewModel,
      child: Consumer<BookViewModel>(
          builder: (context, BookViewModel viewModel, child) {
        BookModel model = viewModel.value;
        return _bodyChild(model);
      }),
    );
  }

  Widget _bodyChild(BookModel model) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Container(
          alignment: Alignment.center,
          width: 200,
          height: 200,
          child: IndexedStack(
            alignment: Alignment.center,
            // 根據載入狀態去顯示對應的佈局
            index: model.loadState,
            children: [
              const CircularProgressIndicator(),
              Text(
                "名字是:${model.name ?? ""} , 作者是:${model.author ?? ""}",
              ),
              // 載入失敗,點選重試
              InkWell(
                child: Image.asset("assets/img/net_error.jpg"),
                onTap: _viewModel.getData,
              )
            ],
          ),
        ),
        Container(
            margin:
                const EdgeInsets.only(top: 50, left: 50, right: 50, bottom: 0),
            height: 50,
            width: MediaQuery.of(context).size.width,
            child: MaterialButton(
              elevation: 3,
              shape: const RoundedRectangleBorder(
                  borderRadius: BorderRadius.all(Radius.circular(20))),
              color: Theme.of(context).primaryColor,
              minWidth: 60,
               // 點選載入
              onPressed: _viewModel.getData,
              child: const Text("請求資料",
                  style: TextStyle(color: Colors.white, fontSize: 20)),
            )),
      ],
    );
  }
}

複製程式碼

效果

1628243569549934.gif

相關文章