Flutter - 實戰指導,使用ScopedModel管理狀態

ScopedModel已通過度到了Provider的模式了。不用深刻本文,就能夠看到ScopedMode裏的VM這一層都是經過調用notifyListeners方法來通知界面更新的,ScopedModelScopedModelDescendant也和Provider模式下的Consumer相差無幾,底層也許有區別不過本質都是一個組件。並且也是用在須要更新的組件子樹上一層來保證更新範圍最小。在VM的組織上基本也是同樣,用VM層來調用各類服務。因此,若是你已經瞭解Provider模式,那麼本片能夠不用看。不瞭解Provider也能夠直接跳過本文看Provider模式。git

本文但願在儘可能接近實戰的條件下能清晰的講解如何使用ScopedModel架構。視頻教程在這裏github

ScopedModel實戰指南

原由

我(做者)在幫一個客戶使用Flutter重製一個App。設計差強人意,性能更是差的離譜。可是我(做者)接手這個項目的時候還只用了Flutter三個星期。調研了ScopedMode和Redux以後就準備用ScopedModel了,BLoC徹底不在考慮範圍內。數據庫

我發現ScopedModel很是容易使用,並且從我開發這個app裏我也有不少的收穫。後端

實現風格

ScopedModel不止有一種實現方式。根據功能組織Model,或者根據頁面來組織Model。兩種方法裏model都須要和服務(service)交互,服務則處理全部的邏輯而且根據返回的數據處理狀態(state)。咱們來快速的過一下這兩種方式。markdown

一個AppModel和FeatureModel mixin

在這個狀況下你有一個AppModel,它會從根組件(root widget)一直傳遞到須要的子組件上。AppModel能夠經過mixin的方式來擴展它所支持的功能好比:網絡

/// Feature model for authentication
class AuthModel extends Model {
    // ...
}

/// App model
class AppModel extends Model with AuthModel {}
複製代碼

若是你仍是不清楚是怎麼回事的話,能夠看這個例子架構

每一個頁面或者組件一個Model

這樣一個ScopedModel就直接和一個頁面或者組件關聯了。可是也會產生不少的固定模式的代碼,畢竟你要爲每一個頁面寫一個Model。app

在(做者)的生產app上,使用了單一AppModel和多個功能mixin的方式。隨着App規模的變大,常常會有一個model處理多個頁面(組件)的狀態的狀況,這樣就有點鬱悶了。因而就遷移到了另一種作法上。每一個頁面/組件一個Model,加上GetIt作爲IoC容器,這樣就簡單了不少。本文的剩餘部分也會繼續講述這個模式。less

若是要動手實踐的話能夠從這個repo裏代代碼開始。用你喜歡的IDE打開start目錄。異步

實現概述

這麼作是爲了更加容易開始,也容易找到切入點。每一個視圖會有一個根Model繼承自ScopedModel。ScopedModel對象將會從locator裏得到。叫作locator是由於它就是用來定位服務和Model的。每一個頁面/組件的model都會代理專門的服務的方法,好比網絡請求或者數據庫操做等,並根據返回的結果更新組件的狀態。

首先,咱們來安裝GetItScopedModel

實現

配置和安裝ScopedModel和依賴注入

在咱們的包清單pubspec裏添加scoped_modelget_it依賴:

...
dependencies:
 flutter:
 sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
 cupertino_icons: ^0.1.2
  # scoped model
 scoped_model: ^1.0.1
  # dependency injection
 get_it: ^1.0.3
...
複製代碼

lib目錄下新建一個service_locator.dart文件。添加以下代碼:

import 'package:get_it/get_it.dart';

GetIt locator = GetIt();

void setupLocator() {
  // Register services

  // Register models
}
複製代碼

你會在這裏註冊你全部的Model和服務對象。以後在main.dart文件裏添加setupLocator()的調用, 以下:

...
import 'service_locator.dart';

void main() {
  // setup locator
  setupLocator();

  runApp(MyApp());
}
...
複製代碼

以上就配置完了app所須要的所有依賴了。

添加組件和Model

咱們來添加一個Home頁面。如今是每一個頁面都有一個scoped model,那麼也新建一個相關的model,並經過locator把他們兩個關聯起來。首先咱們準備好他們要存放的地方。在lib目錄下新建一個ui目錄,在裏面再新建一個view目錄用來存放拆分出來的視圖。

lib目錄下新建scoped_model目錄來存放model。

首先在view目錄下新建一個home_view.dart的文件。

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

class HomeView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModel<HomeModel>(
      child: Scaffold(

    ));
  }
}
複製代碼

咱們須要一個HomeModel來獲取各類咱們須要的對應的信息。在lib/scoped_model目錄先新建home_model.dart文件。

import 'package:scoped_model/scoped_model.dart';

class HomeModel extends Model {
  
}
複製代碼

接下來咱們要把咱們的頁面和scoped model關聯到一塊兒。這個時候就該以前提到的locator上場了。可是,還要完成一些locator的註冊工做。要適用locator就須要先註冊。

import 'package:scoped_guide/scoped_models/home_model.dart';
...

void setupLocator() {
  // register services
  // register models
  locator.registerFactory<HomeModel>(() => HomeModel());
}
複製代碼

HomeModel已經在locator裏完成了註冊。咱們能夠在任何地方經過locator拿到它的實例了。

首先須要引入ScopedModel,這裏用到了泛型,因此它的類型參數就是咱們定義的HomeModel。把它做爲一個組件放進build方法裏。model屬性就用到了locator。在用到HomeModel實例的地方使用ScopedModelDescendant。它也須要一個類型參數,這裏一樣是HomeModel

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_guide/scoped_models/home_model.dart';
import 'package:scoped_guide/service_locator.dart';

class HomeView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModel<HomeModel>(
      model: locator<HomeModel>(),
      child: ScopedModelDescendant<HomeModel>(
        builder: (context, child, model) => Scaffold(
          body: Center(
            child: Text(model.title),
          ),
        )));
  }
}
複製代碼

這裏的model的title屬性能夠設置爲HomeModel

添加服務

新建一個lib/services目錄。這裏咱們會添加一個假的服務,它只會延時兩秒執行,以後返回一個true。添加一個storage_service.dart文件。

class StorageService {
  Future<bool> saveData() async {
    await Future.delayed(Duration(seconds: 2));
    return true;
  }
}
複製代碼

locator裏註冊這個服務:

import 'package:scoped_guide/services/storage_service.dart';
...
void setupLocator() {
  // register services
  locator.registerLazySingleton<StorageService>(() => StorageService());

  // register models
  locator.registerFactory<HomeModel>(() => HomeModel());
}
複製代碼

就如上文所述,咱們用service來完成須要的工做,並使用返回的數據來更新須要更新的組件。可是,這裏還有一個model做爲代理。因此咱們須要用locator來把註冊好的服務和model關聯。

import 'package:scoped_guide/service_locator.dart';
import 'package:scoped_guide/services/storage_service.dart';
import 'package:scoped_model/scoped_model.dart';

class HomeModel extends Model {
  StorageService storageService = locator<StorageService>();

  String title = "HomeModel";

  Future saveData() async {
    setTitle("Saving Data");
    await storageService.saveData();
    setTitle("Data Saved");
  }

  void setTitle(String value) {
    title = value;
    notifyListeners();
  }
}
複製代碼

HomeModel裏的saveData方法纔是組件須要調用到的。這個方法也就是服務的一個大力方法。具體的能夠參考MVVM的模式,這裏就不過多敘述。

saveData方法裏,存數據完成以後調用了setTitle方法。這個方法根據service返回的值設置了title屬性,並調用了notifyListeners方法發出通知。通知須要更新的組件能夠把數據顯示上去了。

HomeViewScaffold裏添加一個浮動按鈕,並在裏面調用HomeModelsaveData方法。那麼,從接收用戶的輸入到「保存數據」,再到最後的更新界面一套流程在代碼裏就所有實現完成了。

回顧一下基礎內容

咱們一塊兒來回顧一下在實際開發中常常會用到的內容。

狀態管理

若是你的app要從網絡或者本地數據庫讀取數據,那麼就會有四個基本狀態須要處理:idel(空閒),busy(獲取數據中),retrieved(成功取得數據)和error。全部的視圖的視圖都會用到這四個狀態,因此比較好的選擇的是在一開始的時候就把他們寫到model裏。

新建lib/enum目錄,在裏面新建一個view_states.dart文件。

/// Represents a view's state from the ScopedModel
enum ViewState {
  Idle,
  Busy,
  Retrieved,
  Error
}
複製代碼

如今視圖的model就能夠引入ViewState了。

import 'package:scoped_guide/service_locator.dart';
import 'package:scoped_guide/services/storage_service.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_guide/enums/view_state.dart';

class HomeModel extends Model {
  StorageService storageService = locator<StorageService>();

  String title = "HomeModel";

  ViewState _state;
  ViewState get state => _state;

  Future saveData() async {
    _setState(ViewState.Busy);
    title = "Saving Data";
    await storageService.saveData();
    title = "Data Saved";
    _setState(ViewState.Retrieved);
  }

  void _setState(ViewState newState) {
    _state = newState;
    notifyListeners();
  }
}
複製代碼

ViewState會經過一個getter暴露出去。一樣的,這些狀態也都須要對應的視圖能夠捕捉到,並在發生變化的時候更新界面。因此,狀態變化的時候也須要調用notifyListeners來通知視圖,或者說更新視圖的狀態。

你能夠看到,狀態變化的時候一個叫作_setState的方法被調用了。這個方法專門去負責調用notifyListeners來通知視圖去作更新。

如今咱們調用了_setStateScopedModel就會收到通知,而後UI裏的某部分就回發生更改。咱們會顯示一個旋轉的菊花來代表服務正在請求數據,也許是經過網絡獲取後端數據也許是本地數據庫的數據。如今來更新一下Scaffold的代碼:

...
body: Center(
    child: Column(
      mainAxisSize: MainAxisSize.min,  
      children: <Widget>[
        _getBodyUi(model.state),
        Text(model.title),
      ]
    )
  )
...
  
Widget _getBodyUi(ViewState state) {
  switch (state) {
    case ViewState.Busy:
      return CircularProgressIndicator();
    case ViewState.Retrieved:
    default:
      return Text('Done');
  }
}  
複製代碼

_getBodyUi方法會更具ViewState的值來顯示不一樣的界面。

多個視圖

一個數據的變化會影響到多個界面的狀況是實際開發中常常發生的。在處理完單個界面更新的簡單狀況後咱們能夠開始處理多個界面的問題了。

在前面的例子中你會看到不少的模板代碼,好比:ScopedModelScopedModelDescendant以及從locator裏獲取model、service之類的對象。這些都是模板代碼,不是不少,可是咱們還可讓它更少。

首先,咱們來新建一個BaseView

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_guide/service_locator.dart';

class BaseView<T extends Model> extends StatelessWidget {
  
  final ScopedModelDescendantBuilder<T> _builder;

  BaseView({ScopedModelDescendantBuilder<T> builder})
      : _builder = builder;

  @override
  Widget build(BuildContext context) {
    return ScopedModel<T>(
        model: locator<T>(), 
        child: ScopedModelDescendant<T>(
          builder: _builder));
  }
}
複製代碼

BaseView裏已經有了ScopedModelScopedModelDescendant的調用。那麼就不不要在每一個界面裏都放這些調用了。好比HomeView就使用BaseView並去掉這些無關的代碼了。

...
import 'base_view.dart';

@override
Widget build(BuildContext context) {
  return BaseView<HomeModel> (
      builder: (context, child, model) => Scaffold(
        ...
  ));
}
複製代碼

這樣咱們能夠用更少的代碼作更多的事了。你能夠給IDE裏註冊一段代碼段,這樣幾個字符輸入了就能夠有一段基本完整的功能的代碼出現了。咱們在lib/ui/views目錄新建一個模板文件template_view.dart

import 'package:flutter/material.dart';
import 'package:scoped_guide/scoped_models/home_model.dart';

import 'base_view.dart';

class Template extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BaseView<HomeModel>(
      builder: (context, child, model) => Scaffold(
         body: Center(child: Text(this.runtimeType.toString()),),
      ));
  }
}
複製代碼

咱們分發出去的狀態也不是隻是專屬於一個界面的,而是能夠多個界面共享的,因此咱們也新建一個BaseModel來處理這個問題。

import 'package:scoped_guide/enums/view_state.dart';
import 'package:scoped_model/scoped_model.dart';

class BaseModel extends Model {
  ViewState _state;
  ViewState get state => _state;

  void setState(ViewState newState) {
    _state = newState;
    notifyListeners();
  }
}
複製代碼

修改HomeModel的代碼,讓他從BaseModel繼承。

...
class HomeModel extends BaseModel {
  ...
  Future saveData() async {
    setState(ViewState.Busy);
    title = "Saving Data";
    await storageService.saveData();
    title = "Data Saved";
    setState(ViewState.Retrieved);
  }
}
複製代碼

對於多個界面的支持的代碼準備都完成了。咱們有BaseViewBaseModel能夠分別服務於視圖和model了。

接下來就是導航了。根據template_view.dart來新建兩個視圖error_view.dartsuccess_view.dart。記得在這些代碼裏面作適當的修改。

接下來新建兩個model,一個是SuccessModel一個是ErrorModel。他們都繼承自BaseModel,而不是Model。而後記得在locator裏面註冊這些model。

導航

基本的導航都很相似。咱們可使用導航器(Navigator)來初始導航棧上的視圖。

如今對咱們的HomeModel#saveData來作一些更改。

Future<bool> saveData() async {
    _setState(ViewState.Busy);
    title = "Saving Data";
    await storageService.saveData();
    title = "Data Saved";
    _setState(ViewState.Retrieved);
    
    return true;
}
複製代碼

HomeView裏,咱們來更新浮動按鈕的onPress方法。讓它成爲一個異步方法,等待saveData執行的結果,並根據結果導航到對應的界面。

floatingActionButton: FloatingActionButton(
    onPressed: () async {
      var whereToNavigate = await model.saveData();
      if (whereToNavigate) {
        Navigator.push(context,MaterialPageRoute(builder: (context) => SuccessView()));
      } else {
        Navigator.push(context,MaterialPageRoute(builder: (context) => ErrorView()));
      }
    }
)
複製代碼

共享的視圖

在多個幾面裏都有獲取數據的服務,那麼他們也就都須要顯示忙碌狀態:一個旋轉的菊花。那麼,這個組件就是能夠在不一樣的界面之間共享的。

新建一個BusyOverlay組件,把它放在lib/ui/views目錄,命名爲busy_overlay.dart

import 'package:flutter/material.dart';

class BusyOverlay extends StatelessWidget {
  final Widget child;
  final String title;
  final bool show;

  const BusyOverlay({this.child,
      this.title = 'Please wait...',
      this.show = false});

  @override
  Widget build(BuildContext context) {
    var screenSize = MediaQuery.of(context).size;
    return Material(
        child: Stack(children: <Widget>[
      child,
      IgnorePointer(
        child: Opacity(
            opacity: show ? 1.0 : 0.0,
            child: Container(
              width: screenSize.width,
              height: screenSize.height,
              alignment: Alignment.center,
              color: Color.fromARGB(100, 0, 0, 0),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  CircularProgressIndicator(),
                  Text(title,
                      style: TextStyle(
                          fontSize: 16.0,
                          fontWeight: FontWeight.bold,
                          color: Colors.white)),
                ],
              ),
            )),
      ),
    ]));
  }
}
複製代碼

如今咱們能夠界面裏使用這個組件了。在HomeView裏,把Scaffold放進BusyOverlay裏面:

@override
Widget build(BuildContext context) {
  return BaseView<HomeModel>(builder: (context, child, model) =>
     BusyOverlay(
      show: model.state == ViewState.Busy,
      child: Scaffold(
      ...
      )));
}
複製代碼

如今,當你點擊浮動按鈕的時候你會看到一個「請稍等」的提示。你也能夠把BusyOverlay組件的調用放進BaseView裏面。記住你的忙碌提示組要在builder裏面,這樣它才能更具model的返回值做出正確的反應。

異步問題的處理

咱們已經處理了根據不一樣的model返回值來顯示對應的界面。如今咱們要處理另一個常見的問題,那就是異步問題的處理。

加載頁面,並獲取數據

當你有一個列表,點了某行要看到更多的詳細信息的時候基本就會遇到一個異步場景。當進入詳情頁面的時候,咱們就會根據傳過來的這個特定數據的ID等相關數據來請求後端得到更多的詳細數據。

請求通常都是發生在StatefulWidgetinitState方法內。本例不打算添加太多的界面,咱們只關注在架構上面。咱們會寫死一個返回值,讓這個值在「請求成功」的時候返回給界面。

首先,咱們來更新SuccessModel

import 'package:scoped_guide/scoped_models/base_model.dart';

class SuccessModel extends BaseModel {
  String title = "no text yet";

  Future fetchDuplicatedText(String text) async {
    setState(ViewState.Busy);
    await Future.delayed(Duration(seconds: 2));
    title = '$text $text';

    setState(ViewState.Retrieved);
  }
}
複製代碼

如今咱們能夠在視圖建立的時候調用model的方法了。不過這須要咱們把BaseView換成StatefulWidget。在BaseViewinitState方法裏調用model的異步方法。

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_guide/service_locator.dart';

class BaseView<T extends Model> extends StatefulWidget {
  final ScopedModelDescendantBuilder<T> _builder;
  final Function(T) onModelReady;

  BaseView({ScopedModelDescendantBuilder<T> builder, this.onModelReady})
      : _builder = builder;

  @override
  _BaseViewState<T> createState() => _BaseViewState<T>();
}

class _BaseViewState<T extends Model> extends State<BaseView<T>> {
  T _model = locator<T>();

  @override
  void initState() {
    if(widget.onModelReady != null) {
      widget.onModelReady(_model);
    }
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return ScopedModel<T>(
        model: _model, 
        child: ScopedModelDescendant<T>(
          child: Container(color: Colors.red),
          builder: widget._builder));
  }
}
複製代碼

而後更新你的SuccessView,在onMondelReady屬性裏傳入你要調用的方法。

class SuccessView extends StatelessWidget {
  final String title;

  SuccessView({this.title});

  @override
  Widget build(BuildContext context) {
    return BaseView<SuccessModel>(
        onModelReady: (model) => model.fetchDuplicatedText(title),
        builder: (context, child, model) => BusyOverlay(
            show: model.state == ViewState.Busy,
            child: Scaffold(
              body: Center(child: Text(model.title)),
            )));
  }
}
複製代碼

最後在導航的時候傳入參數。

Navigator.push(context, MaterialPageRoute(builder: (context) = > SuccessView(title: 'Pass in from home')));
複製代碼

這樣就能夠了。如今你能夠在ScopedModel架構下跑起來你的app了。

所有完成

本文基本覆蓋了使用ScopedModel開發app所須要的所有內容。在這個時候你已經能夠來實現你本身的服務了。一個很重要可是本文沒有提到的問題是測試。

咱們也能夠經過構造函數來實現依賴注入,好比經過構造函數的依賴注入來往model裏注入service。這樣咱們也能夠注入一些假的service。我(做者)是沒有對model層作測試的,由於他們都是徹底的依賴於服務層。而服務層我都作了充分的測試。

相關文章
相關標籤/搜索