文章原文地址:Nealyang/PersonalBlog前端
可能做爲一個前端,在學習 Flutter 的過程當中,總感受很是很是類似 React Native,甚至於,其中仍是有state的概念 setState
,因此在 Flutter 中,也固然會存在很是多的解決方案,好比 redux 、RxDart 還有 Scoped Model等解決方案。今天,咱們主要介紹下經常使用的兩種 State 管理解決方案:redux、scoped model。react
Scoped Model 是 package 上 Dart 的一個第三方庫scoped_model。Scoped Model 主要是經過數據model的概念來實現數據傳遞,表現上相似於 react 中 context 的概念。它提供了讓子代widget輕鬆獲取父級數據model的功能。git
從官網中的介紹能夠了解到,它直接來自於Google正在開發的新系統Fuchsia核心 Widgets 中對 Model 類的簡單提取,做爲獨立使用的獨立 Flutter 插件發佈。github
在直接上手以前,咱們先着重說一下 Scoped Model 中幾個重要的概念web
固然,在 Scoped Model 的文檔中,也介紹了一些 實現原理redux
Model類實現了Listenable接口網絡
demo地址app
從gif上能夠看到我們的需求很是的簡單,就是在當前頁面更新了count後,在第二個頁面也可以傳遞過去。固然,new ResultPage(count:count)
就沒意思啦~ 咱不討論哈less
lib/model/counter_model.dart
import 'package:scoped_model/scoped_model.dart'; class CounterModel extends Model{ int _counter = 0; int get counter => _counter; void increment(){ _counter++; // 通知全部的 listener notifyListeners(); } }
Model
get
方法,以便於後面取數據modelincrement
方法,去改變咱們的數據 model ,調用 package 中的 通知方法 notifyListeners
lib/main.dart
import 'package:flutter/material.dart'; import './model/counter_model.dart'; import 'package:scoped_model/scoped_model.dart'; import './count_page.dart'; void main() { runApp(MyApp( model: CounterModel(), )); } class MyApp extends StatelessWidget { final CounterModel model; const MyApp({Key key,@required this.model}):super(key:key); @override Widget build(BuildContext context) { return ScopedModel( model: model, child: MaterialApp( title: 'Scoped Model Demo', home:CountPage(), ), ); } }
這是 app 的入口文件,劃重點async
MyApp
類 中,咱們傳入一個定義好的數據 model ,方便後面傳遞給子類MaterialApp
用 ScopedModel
包裹一下,做用上面已經介紹了,方便子類能夠拿到 ,相似於 redux
中 Provider
包裹一下model
傳遞給 ScopedModel
的 model 屬性中lib/count_page.dart
class CountPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Scoped Model'), actions: <Widget>[ IconButton( tooltip: 'to result', icon: Icon(Icons.home), onPressed: (){ Navigator.push(context,MaterialPageRoute(builder: (context)=>ResultPage())); }, ) ], ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('你都點擊'), ScopedModelDescendant<CounterModel>( builder: (context, child, model) { return Text( '${model.counter.toString()} 次了', style: TextStyle( color: Colors.red, fontSize: 33.0, ), ); }, ) ], ), ), floatingActionButton: ScopedModelDescendant<CounterModel>( builder: (context,child,model){ return FloatingActionButton( onPressed: model.increment, tooltip: 'add', child: Icon(Icons.add), ); }, ), ); } }
常規佈局和widget這裏再也不重複介紹,咱們說下主角:Scoped Model
ScopedModelDescendant
ScopedModelDescendant
中的build方法須要返回一個widget,在這個widget中咱們可使用數據 model中的方法、數據等最後在 lib/result_page.dart
中就能夠看到咱們數據 model 中的 count 值了,注意這裏跳轉頁面,咱們並無經過參數傳遞的形式傳遞 Navigator.push(context,MaterialPageRoute(builder: (context)=>ResultPage()));
完整項目代碼:flutter_scoped_model
相信做爲一個前端對於 redux 必定不會陌生,而 Flutter 中也一樣存在 state 的概念,其實說白了,UI 只是數據(state)的另外一種展示形式。study-redux是筆者以前學習redux時候的一些筆記和心得。這裏爲了防止有新人不太清楚redux,咱們再來介紹下redux的一些基本概念
state 咱們能夠理解爲前端UI的狀態(數據)庫,它存儲着這個應用全部須要的數據。
既然這些state已經有了,那麼咱們是如何實現管理這些state中的數據的呢,固然,這裏就要說到action了。 什麼是action?E:action:動做。 是的,就是這麼簡單。。。
只有當某一個動做發生的時候纔可以觸發這個state去改變,那麼,觸發state變化的緣由那麼多,好比這裏的咱們的點擊事件,還有網絡請求,頁面進入,鼠標移入。。。因此action的出現,就是爲了把這些操做所產生或者改變的數據從應用傳到store中的有效載荷。 須要說明的是,action是state的惟一信號來源。
reducer決定了state的最終格式。 reducer是一個純函數,也就是說,只要傳入參數相同,返回計算獲得的下一個 state 就必定相同。沒有特殊狀況、沒有反作用,沒有 API 請求、沒有變量修改,單純執行計算。reducer對傳入的action進行判斷,而後返回一個經過判斷後的state,這就是reducer的所有職責。 從代碼能夠簡單地看出:
import {INCREMENT_COUNTER,DECREMENT_COUNTER} from '../actions'; export default function counter(state = 0,action) { switch (action.type){ case INCREMENT_COUNTER: return state+1; case DECREMENT_COUNTER: return state-1; default: return state; } }
對於一個比較大一點的應用來講,咱們是須要將reducer拆分的,最後經過redux提供的combineReducers方法組合到一塊兒。 好比:
const rootReducer = combineReducers({ counter }); export default rootReducer;
這裏你要明白:每一個 reducer 只負責管理全局 state 中它負責的一部分。每一個 reducer 的 state 參數都不一樣,分別對應它管理的那部分 state 數據。 combineReducers() 所作的只是生成一個函數,這個函數來調用你的一系列 reducer,每一個 reducer 根據它們的 key 來篩選出 state 中的一部分數據並處理, 而後這個生成的函數再將全部 reducer 的結果合併成一個大的對象。
store是對以前說到一個聯繫和管理。具備以下職責
再次強調一下 Redux 應用只有一個單一的 store。當須要拆分數據處理邏輯時,你應該使用 reducer 組合 而不是建立多個 store。 store的建立經過redux的createStore方法建立,這個方法還須要傳入reducer,很容易理解:畢竟我須要dispatch一個action來改變state嘛。 應用通常會有一個初始化的state,因此可選爲第二個參數,這個參數一般是有服務端提供的,傳說中的Universal渲染。後面會說。。。 第三個參數通常是須要使用的中間件,經過applyMiddleware傳入。
說了這麼多,action,store,action creator,reducer關係就是這麼以下的簡單明瞭:
一些工具集讓你輕鬆地使用 redux 來輕鬆構建 Flutter widget,版本要求是 redux.dart 3.0.0+
StoreProvider ancestor,使用給定的 converter 函數將 Store 轉換爲 ViewModel ,並將ViewModel傳遞給 builder。 只要 Store 發出更改事件(action),Widget就會自動重建。 無需管理訂閱!
Dart 2須要更嚴格的類型!
一、確認你正使用的是 redux 3.0.0+
二、在你的組件樹中,將 new StoreProvider(...)
改成 new StoreProvider<StateClass>(...)
三、若是須要從StoreProvider<AppState>
中直接獲取 Store<AppState>
,則須要將 new StoreProvider.of(context)
改成 StoreProvider.of<StateClass>
.不須要直接訪問 Store 中的字段,由於Dart2可使用靜態函數推斷出正確的類型
官方demo的代碼先大概解釋一下
import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; //定義一個action: Increment enum Actions { Increment } // 定義一個 reducer,響應傳進來的 action int counterReducer(int state, dynamic action) { if (action == Actions.Increment) { return state + 1; } return state; } void main() { // 在 基礎 widget 中建立一個 store,用final關鍵字修飾 這比直接在build方法中建立要好不少 final store = new Store<int>(counterReducer, initialState: 0); runApp(new FlutterReduxApp( title: 'Flutter Redux Demo', store: store, )); } class FlutterReduxApp extends StatelessWidget { final Store<int> store; final String title; FlutterReduxApp({Key key, this.store, this.title}) : super(key: key); @override Widget build(BuildContext context) { // 用 StoreProvider 來包裹你的 MaterialApp 或者別的 widget ,這樣可以確保下面全部的widget可以獲取到store中的數據 return new StoreProvider<int>( // 將 store 傳遞給 StoreProvider // Widgets 將使用 store 變量來使用它 store: store, child: new MaterialApp( theme: new ThemeData.dark(), title: title, home: new Scaffold( appBar: new AppBar( title: new Text(title), ), body: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: [ new Text( 'You have pushed the button this many times:', ), // 經過 StoreConnector 將 store 和 Text 鏈接起來,以便於 Text直接render // store 中的值。相似於 react-redux 中的connect // // 將 Text widget 包裹在 StoreConnector 中, // `StoreConnector`將會在最近的一個祖先元素中找到 StoreProvider // 拿到對應的值,而後傳遞給build函數 // // 每次點擊按鈕的時候,將會 dispatch 一個 action而且被reducer所接受。 // 等reducer處理得出最新結果後, widget將會自動重建 new StoreConnector<int, String>( converter: (store) => store.state.toString(), builder: (context, count) { return new Text( count, style: Theme.of(context).textTheme.display1, ); }, ) ], ), ), // 一樣使用 StoreConnector 來鏈接Store 和FloatingActionButton // 在這個demo中,咱們使用store 去構建一個包含dispatch、Increment // action的回調函數 // // 將這個回調函數丟給 onPressed floatingActionButton: new StoreConnector<int, VoidCallback>( converter: (store) { return () => store.dispatch(Actions.Increment); }, builder: (context, callback) { return new FloatingActionButton( onPressed: callback, tooltip: 'Increment', child: new Icon(Icons.add), ); }, ), ), ), ); } }
上面的例子比較簡單,鑑於小冊Flutter入門實戰:從0到1仿寫web版掘金App下面有哥們在登錄那塊評論了Flutter狀態管理,
這裏我簡單使用redux模擬了一個登錄的demo
lib/reducer/reducers.dart
首先咱們定義action須要的一些action type
enum Actions{ Login, LoginSuccess, LogoutSuccess }
而後定義相應的類來管理登錄狀態
class AuthState{ bool isLogin; //是否登陸 String account; //用戶名 AuthState({this.isLogin:false,this.account}); @override String toString() { return "{account:$account,isLogin:$isLogin}"; } }
而後咱們須要定義一些action,定義個基類,而後定義登錄成功的action
class Action{ final Actions type; Action({this.type}); } class LoginSuccessAction extends Action{ final String account; LoginSuccessAction({ this.account }):super( type:Actions.LoginSuccess ); }
最後定義 AppState
以及咱們自定義的一箇中間件。
// 應用程序狀態 class AppState { AuthState auth; //登陸 MainPageState main; //主頁 AppState({this.main, this.auth}); @override String toString() { return "{auth:$auth,main:$main}"; } } AppState mainReducer(AppState state, dynamic action) { if (Actions.LogoutSuccess == action) { state.auth.isLogin = false; state.auth.account = null; } if (action is LoginSuccessAction) { state.auth.isLogin = true; state.auth.account = action.account; } print("state changed:$state"); return state; } loggingMiddleware(Store<AppState> store, action, NextDispatcher next) { print('${new DateTime.now()}: $action'); next(action); }
在稍微大一點的項目中,其實就是reducer 、 state 和 action 的組織會比較麻煩,固然,羅馬也不是一日建成的, 龐大的state也是一點一點累計起來的。
下面就是在入口文件中使用 redux 的代碼了,跟基礎demo沒有差別。
import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; import 'dart:async' as Async; import './reducer/reducers.dart'; import './login_page.dart'; void main() { Store<AppState> store = Store<AppState>(mainReducer, initialState: AppState( main: MainPageState(), auth: AuthState(), ), middleware: [loggingMiddleware]); runApp(new MyApp( store: store, )); } class MyApp extends StatelessWidget { final Store<AppState> store; MyApp({Key key, this.store}) : super(key: key); @override Widget build(BuildContext context) { return new StoreProvider(store: store, child: new MaterialApp( title: 'Flutter Demo', theme: new ThemeData( primarySwatch: Colors.blue, ), home: new StoreConnector<AppState,AppState>(builder: (BuildContext context,AppState state){ print("isLogin:${state.auth.isLogin}"); return new MyHomePage(title: 'Flutter Demo Home Page', counter:state.main.counter, isLogin: state.auth.isLogin, account:state.auth.account); }, converter: (Store<AppState> store){ return store.state; }) , routes: { "login":(BuildContext context)=>new StoreConnector(builder: ( BuildContext context,Store<AppState> store ){ return new LoginPage(callLogin: (String account,String pwd) async{ print("正在登陸,帳號$account,密碼:$pwd"); // 爲了模擬實際登陸,這裏等待一秒 await new Async.Future.delayed(new Duration(milliseconds: 1000)); if(pwd != "123456"){ throw ("登陸失敗,密碼必須是123456"); } print("登陸成功!"); store.dispatch(new LoginSuccessAction(account: account)); },); }, converter: (Store<AppState> store){ return store; }), }, )); } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title, this.counter, this.isLogin, this.account}) : super(key: key); final String title; final int counter; final bool isLogin; final String account; @override Widget build(BuildContext context) { print("build:$isLogin"); Widget loginPane; if (isLogin) { loginPane = new StoreConnector( key: new ValueKey("login"), builder: (BuildContext context, VoidCallback logout) { return new RaisedButton( onPressed: logout, child: new Text("您好:$account,點擊退出"),); }, converter: (Store<AppState> store) { return () => store.dispatch( Actions.LogoutSuccess ); }); } else { loginPane = new RaisedButton(onPressed: () { Navigator.of(context).pushNamed("login"); }, child: new Text("登陸"),); } return new Scaffold( appBar: new AppBar( title: new Text(title), ), body: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ /// 有登陸,展現你好:xxx,沒登陸,展現登陸按鈕 loginPane ], ), ), ); } }
完整項目代碼:Nealyang/Flutter
更多學習 Flutter的小夥伴,歡迎入QQ羣 Flutter Go :679476515
關於 Flutter 組件以及更多的學習,敬請關注咱們正在開發的: alibaba/flutter-go
flutter_architecture_samples
flutter_redux
flutter example
scoped_model