基於 Redux + Redux Persist 進行狀態管理的 Flutter 應用示例

pantao發表於2018-12-17

好久沒在 SegmentFault 寫東西,唉,也不知道 是忙還是懶,以後有時間 再慢慢寫起來吧,最近開始學點新東西,有的寫了,個人部落格跟這裡同步。

一直都在自己的 React Native 應用中使用 Redux,其實更大情況下也是使用它來管理應用的會話狀態以及當前登入的使用者資訊等等簡單的資料,很好用,自從 Google 釋出 Flutter 之後,就一直想著拿它來做點啥,準備拿一個新專案開刀,先研究下怎麼把以前在 React Native 中需要用到的一些技術在 Flutter 找到對應的實現方法,本文記錄下 Flutter + Redux + Redux Persist 的實現。

原文地址:Flutter + Redux + Redux Persist 應用
專案地址:https://github.com/pantao/flutter-redux-demo-app

<!–more–>

第一步:建立一個新的應用:redux_demo_app

flutter create redux_demo_app
cd redux_demo_app
code .

Flutter 專案必須是一個合法的 Dart 包,而 Dart 包要求使用純小寫字母(可包含下劃線),這個跟 React Native 是不一樣的。

第二步:新增依懶

我們依懶下面這些包:

開啟 pubspec.yaml,在 dependencies 中新增下面這些依懶:

...
dependencies:
  ...
  redux: ^3.0.0
  flutter_redux: ^0.5.2
  redux_persist: ^0.8.0
  redux_persist_flutter: ^0.8.0

dev_dependencies:
  ...
...

第三步:瞭解需求

本次我想做的一個App有下面四個頁面:

  • 首頁
  • 個人中心頁
  • 個人資料詳情頁
  • 登入頁

互動是下面這樣的:

  • 應用開啟之後,開啟的是一個有兩個底部 Tab 的應用,預設展示的是首頁
  • 當使用者點選(我的)這個Tab時:

    • 若當前使用者已登入,則Tab切換為個人中心頁
    • 若當前使用者未登入,則以 Modal 的方式彈出登入頁

新增 lib/state.dart 檔案

內容如下:

enum Actions{
  login,
  logout
}

/// App 狀態
/// 
/// 狀態中所有資料都應該是隻讀的,所以,全部以 get 的方式提供對外訪問,不提供 set 方法
class AppState {
  /// J.W.T
  String _authorizationToken;

  // 獲取當前的認證 Token
  get authorizationToken => _authorizationToken;

  // 獲取當前是否處於已認證狀態
  get authed => _authorizationToken.length > 0;

  AppState(this._authorizationToken);
}

/// Reducer
AppState reducer(AppState state, action) {
  switch(action) {
    case Actions.login:
      return AppState(`J.W.T`);
    case Actions.logout:
      return AppState(``);
    default:
      return state;
  }
}

在上面的程式碼中,我們先宣告瞭 Actions 列舉,以及一個 AppState 類,該類就是我們的應用狀態類,使用 _authorizationToken 保證認證的值不可被例項外直接被訪問到,這樣使用者就無法去直接修改它的值,再提供了兩個 get 方法,提供給外部訪問它的值。

接著我們定義了一個 reducer 函式,用於更新狀態。

建立 app.dart

import `package:flutter/material.dart`;

import `package:redux/redux.dart`;
import `package:flutter_redux/flutter_redux.dart`;

import `state.dart`;
import `root.dart`;

/// 示例App
class DemoApp extends StatelessWidget {

  // app store
  final Store<AppState> store;

  DemoApp(this.store);

  @override
  Widget build(BuildContext context) {
    return StoreProvider<AppState>(
      store: store,
      child: new MaterialApp(
        title: `Flutter Redux Demo App`,
        // home 為 root 頁
        home: Root()
      ),
    );
  }
}

在上面我們已經完成的 App 類的編碼,現在需要完成 Root 頁,也就是我們的App入口頁。

建立 Root

import `package:flutter/material.dart`;
import `package:redux/redux.dart`;
import `package:flutter_redux/flutter_redux.dart`;

/// 狀態
import `state.dart`;
/// 登入頁面
import `auth.dart`;
/// 我的頁面
import `me.dart`;
/// 首頁
import `home.dart`;

/// 應用入口頁
class Root extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
      return _RootState();
    }
}

/// 入口頁狀態
class _RootState extends State<Root> {
  /// 當前被啟用的 Tab Index
  int _currentTabIndex;
  /// 所有 Tab 列表頁
  List<Widget> _tabPages;

  @override
  void initState() {
    super.initState();
    // 初始化 tab 為第 0 個
    _currentTabIndex = 0;
    // 初始化頁面列表
    _tabPages = <Widget>[
      // 首頁
      Home(),
      // 我的
      Me()
    ];
  }

  @override
  Widget build(BuildContext context) {
    // 使用 StoreConnector 建立 Widget
    // 類似於 React Redux  的 connect,連結 store state 與 Widget
    return StoreConnector<AppState, Store<AppState>>(
      // store 轉換器,類似於 react redux 中的 mapStateToProps 方法
      // 接受引數為 `store`,再返回的資料可以被在 `builder` 函式中使用,
      // 在此處,我們直接返回整個 store,
      converter: (store) => store,
      // 構建器,第二個引數 store 就是上一個 converter 函式返回的 store
      builder: (context, store) {
        // 取得當前是否已登入狀態
        final authed = store.state.authed;
        return new Scaffold(
          // 如果已登入,則直接可以訪問所有頁面,否則展示 Home
          body: authed ? _tabPages[_currentTabIndex] : Home(),
          // 底部Tab航
          bottomNavigationBar: BottomNavigationBar(
            onTap: (int index) {
              // 如果點選的是第 1 個Tab,且當前使用者未登入,則直接開啟登入 Modal 頁
              if (!authed && index == 1) {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => Auth(),
                    fullscreenDialog: true
                  )
                );
              // 否則直接進入相應頁面
              } else {
                setState(() {
                  _currentTabIndex = index;
                });
              }
            },
            // 與 body 取值方式類似
            currentIndex: authed ? _currentTabIndex : 0,
            items: [
              BottomNavigationBarItem(
                icon: Icon(Icons.home),
                title: Text(`首頁`)
              ),
              BottomNavigationBarItem(
                icon: Icon(Icons.people),
                title: Text(`我的`)
              )
            ],
          ),
        );
      },
    );
  }
}

建立 Home

Root 頁面類似,我們可以在任何頁面方便的使用 AppState

import `package:flutter/material.dart`;
import `package:redux/redux.dart`;
import `package:flutter_redux/flutter_redux.dart`;

import `state.dart`;
import `auth.dart`;

class Home extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, Store<AppState>>(
      converter: (store) => store,
      builder: (context, store) {
        return Scaffold(
          appBar: AppBar(
            title: Text(`首頁`),
          ),
          body: Center(
            child: store.state.authed
              ? Text(`您已登入`)
              : FlatButton(
                child: Text(`去登入`),
                onPressed: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) => Auth(),
                      fullscreenDialog: true
                    )
                  );
                },
              )
          ),
        );
      },
    );
  }
}

完成 Auth

在前面的所有頁面中,都只是對 store 中狀態樹的讀取,現在的 Auth 就需要完成對狀態樹的更新了,看下面程式碼:

import `package:flutter/material.dart`;
import `package:redux/redux.dart`;
import `package:flutter_redux/flutter_redux.dart`;
import `state.dart`;

class Auth extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _AuthState();
}

class _AuthState extends State<Auth> {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, Store<AppState>>(
      converter: (store) => store,
      builder: (context, store) {
        return Scaffold(
          appBar: AppBar(
            title: Text(`登入`),
          ),
          body: Center(
            child: FlatButton(
              child: Text(`登入`),
              onPressed: () {
                // 通過 store.dispatch 函式,可以發出 action(跟 Redux 是一樣的),而 Action 是在
                // AppState 中定義的列舉 Actions.login
                store.dispatch(Actions.login);
                // 之後,關閉當前的 Modal,就可以看到應用所有資料都更新了
                Navigator.pop(context);
              },
            )
          ),
        );
      },
    );
  }
}

建立 Me

有了登入之後,我們可以在做一個我的頁面,在這個頁面裡面我們可以完成退出功能。

import `package:flutter/material.dart`;
import `package:redux/redux.dart`;
import `package:flutter_redux/flutter_redux.dart`;
import `state.dart`;

class Me extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _MeState();
}

class _MeState extends State<Me> {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, Store<AppState>>(
      converter: (store) => store,
      builder: (context, store) {
        return Scaffold(
          appBar: AppBar(
            title: Text(`退出`),
          ),
          body: Center(
            child: FlatButton(
              child: Text(`退出`),
              onPressed: () {
                store.dispatch(Actions.logout);
                // 此處我們不需要去更新Tab Index,在 Root 頁面中,對 store 裡面的 authed 值已經做了監聽,如果
                // Actions.logout 被觸發後, authed 的值會變成 false,那麼App將自動切換首頁
              },
            )
          ),
        );
      },
    );
  }
}

新增狀態持久化

在上面,我們已經完成了一個基於 Redux 的同步狀態的App,但是當你的App關閉重新開啟之外,狀態樹就會被重置為初始值,這並不理想,我們經常需要一個使用者完成登入之後,就可以在一斷時間內一直保持這個登入狀態,而且有一些資料我們並不希望每次開啟App的時候都重新初始化一次,這個時候,可以考慮對狀態進行持久化了。

更新 state.dart

class AppState {
  ...
  // 持久化時,從 JSON 中初始化新的狀態
  static AppState fromJson(dynamic json) => json != null ? AppState(json[`authorizationToken`] as String) : AppState(``);

  // 更新狀態之後,轉成 JSON,然後持久化至持久化引擎中
  dynamic toJson() => {`authorizationToken`: _authorizationToken};
}

這裡我們新增了兩個方法,一個是靜態的 fromJson 方法,它將在初始化狀態樹時被呼叫,用於從 JSON 中初始化一個新的狀態樹出來, toJson 將被用於持久化,將自身轉成 JSON。

更新 main.dart

import `package:flutter/material.dart`;
import `package:redux/redux.dart`;
import `package:redux_persist/redux_persist.dart`;
import `package:redux_persist_flutter/redux_persist_flutter.dart`;

import `app.dart`;
import `state.dart`;

void main() async {
  // 建立一個持久化器
  final persistor = Persistor<AppState>(
    storage: FlutterStorage(),
    serializer: JsonSerializer<AppState>(AppState.fromJson),
    debug: true
  );

  // 從 persistor 中載入上一次儲存的狀態
  final initialState = await persistor.load();

  final store = Store<AppState>(
    reducer,
    initialState: initialState ?? AppState(``),
    middleware: [persistor.createMiddleware()]
  );
  runApp(new DemoApp(store));
}

重新 flutter run 當前應用,即完成了持久化,可以登入,然後退出應用,再重新開啟應用,可以看到上一次的登入狀態是存在的。

相關文章