手把手入門Fish-Redux開發flutter(下)

擁小抱 發表於 2019-10-15

手把手入門Fish-Redux開發flutter(上) (中) (下)

前面兩篇,我們瞭解了fish-redux並實現了簡單的功能,這次我們再瞭解fish-redux一些其他的特點。看一下結果圖:

手把手入門Fish-Redux開發flutter(下)

1 使用 Component 和 Adapter 做一個列表

1.1 建立列表頁、定義資料

頁面建立的過程跟之前一樣,就省略啦。我建立了名為 List 的頁面,結果如下

手把手入門Fish-Redux開發flutter(下)

在 app.dart 中加入我們的這個頁面。修改過的 app.dart 如下

Widget createApp() {
  final AbstractRoutes routes = PageRoutes(
    pages: <String, Page<Object, dynamic>>{
      'entrance_page': EntrancePage(),
      'grid_page': GridPage(),
      'list_page':ListPage(),   //此處加入我們新建的頁面
    },
  );
//省略...
複製程式碼

然後實現從 Grid 頁面跳轉到這個頁面,(頁面跳轉上一篇講過了,這裡不詳細說。就是建立action、在view中dispatch action、effect中接收到action並跳轉頁面)程式碼如下

手把手入門Fish-Redux開發flutter(下)

手把手入門Fish-Redux開發flutter(下)

手把手入門Fish-Redux開發flutter(下)

1.2 建立 component

然後我們在 list 包下面建立一個 Component 作為列表的每個 item 。

第一步 通過外掛新建Component

首先建立一個名為 item 的包,然後在 item 下建立FishReduxTemplate,這次我們選擇 Component

手把手入門Fish-Redux開發flutter(下)

手把手入門Fish-Redux開發flutter(下)

建立結果如下,可以看到元件中的 component.dart 類似頁面中的 page.dart。

手把手入門Fish-Redux開發flutter(下)

第二步 定義元件資料和ui

我們給 state 三個欄位 type(圖示的形狀),title(標題),content(內容)。修改 /list/item/state.dart 如下

import 'package:fish_redux/fish_redux.dart';

class ItemState implements Cloneable<ItemState> {

  int type;
  String title;
  String content;

  ItemState({this.type, this.title, this.content});

  @override
  ItemState clone() {
    return ItemState()
    ..type = type
    ..title = title
    ..content = content;
  }
}

ItemState initState(Map<String, dynamic> args) {
  return ItemState();
}
複製程式碼

然後我們來實現 item 的檢視,使用上面 state 的資料,並且根據 type 不同顯示不同的 icon 圖示(詳見註釋)。/list/item/view.dart 如下

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

import 'action.dart';
import 'state.dart';

Widget buildView(ItemState state, Dispatch dispatch, ViewService viewService) {
  return Container(
    margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
    height: 120.0,
    color: Colors.white,
    child: GestureDetector(
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          //左側圖示
          Container(
            padding: const EdgeInsets.only(right: 5.0),
            child: Center(
              child: Icon(
              //不同type顯示不同icon
                state.type == 1 ? Icons.account_circle : Icons.account_box,
                size: 50.0,
              ),
            ),
          ),
          //右側
          Expanded(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.start,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                //標題部分
                Container(
                  height: 30,
                  child: Text(
                    state.title,
                    style: TextStyle(fontSize: 22.0),
                  ),
                ),
                //內容部分
                Text(
                  state.content,
                  style: TextStyle(fontSize: 16.0),
                ),
              ],
            ),
          ),
        ],
      ),
      onTap: () {
        //todo 點選事件
      },
    ),
  );
}
複製程式碼

1.3 關聯元件和頁面

Component告一段落。接著我們在列表中使用元件。

第一步 建立adapter

首先在 List 的 State 中儲存每一個 Item 的 State 資料。/list/state.dart/ 如下

import 'package:fish_demo/list/item/state.dart';
import 'package:fish_redux/fish_redux.dart';

class ListState implements Cloneable<ListState> {
  
  List<ItemState> items;    //儲存item的state

  @override
  ListState clone() {
    return ListState()
    ..items = items;
  }
}

ListState initState(Map<String, dynamic> args) {
  return ListState();
}
複製程式碼

然後把 list 和 item 關聯,我們使用 fish-redux 的一個元件叫 adapter。首先在 list 包下用外掛建立 FishReduxTemplate,然後選中 DynamicFlowAdapter,並取消其他的勾選,取名 List,如下

手把手入門Fish-Redux開發flutter(下)

第二步 實現connector

看到預設的 adapter 程式碼中為我們準備了兩個類:一個 ListAdapter 和一個 _ListConnector。其中 ListAdapter 中我們配置需要關聯的元件,即把元件新增到 pool。以及列表和元件的資料適配關係,通過實現一個 connector 。完成的 /list/adapter.dart 如下

import 'package:fish_demo/list/state.dart';
import 'package:fish_redux/fish_redux.dart';

import 'item/component.dart';
import 'item/state.dart';

class ListAdapter extends DynamicFlowAdapter<ListState> {
  ListAdapter()
      : super(
          pool: <String, Component<Object>>{
            "MyItem": ItemComponent(),  //引用元件
          },
          connector: _ListConnector(),
        );
}

class _ListConnector extends ConnOp<ListState, List<ItemBean>> {
  @override
  List<ItemBean> get(ListState state) {
    //判斷ListState裡面的items資料是否為空
    if (state.items?.isNotEmpty == true) {
      //若不為空,把item資料轉化成ItemBean的列表
      return state.items
        .map<ItemBean>((ItemState data) => ItemBean('MyItem', data))
        .toList(growable: true);
    }else{
      //若為空,返回空列表
      return <ItemBean>[];
    }
  }

  @override
  void set(ListState state, List<ItemBean> items) {
    //把ItemBean的變化,修改到item的state的過程
    if (items?.isNotEmpty == true) {
      state.items = List<ItemState>.from(
        items.map<ItemState>((ItemBean bean) => bean.data).toList());
    } else {
      state.items = <ItemState>[];
    }
  }

  @override
  subReducer(reducer) {
    // TODO: implement subReducer
    return super.subReducer(reducer);
  }
}
複製程式碼

第三步 把adapter新增到列表頁的依賴中

我們開啟 List 頁面的 page 檔案,然後吧adapter新增到頁面的 dependencies 中。如下

import 'package:fish_demo/list/adapter.dart';//注意1
import 'package:fish_redux/fish_redux.dart' hide ListAdapter;//注意1

import 'effect.dart';
import 'reducer.dart';
import 'state.dart';
import 'view.dart';

class ListPage extends Page<ListState, Map<String, dynamic>> {
  ListPage()
      : super(
            initState: initState,
            effect: buildEffect(),
            reducer: buildReducer(),
            view: buildView,
            dependencies: Dependencies<ListState>(
                adapter: NoneConn<ListState>() + ListAdapter(),//注意2
                slots: <String, Dependent<ListState>>{
                }),
            middleware: <Middleware<ListState>>[
            ],);

}
複製程式碼

上面的程式碼我標註了兩點注意。

注意1:我們這裡引用的是剛才自定義的ListAdapter而不是fish-redux自帶的ListAdapter類,這裡要處理一下引用。(早知道就起名字的時候就不叫ListAdapter了。。。)

注意2:這裡使用的"加號"是 fish-redux 過載的操作符。後者是我們自定義的 adapter ,而由於我們在外層已經不需要使用 connector,所以前者傳入一個 NoneConn

第四步 列表ui

最後我們在 /list/view.dart 獲取到 adapter 並完成列表頁的UI。/list/view.dart 如下

import 'package:fish_redux/fish_redux.dart';
import 'package:flutter/material.dart';
import 'state.dart';

Widget buildView(ListState state, Dispatch dispatch, ViewService viewService) {
  
  ListAdapter adapter = viewService.buildAdapter();     //建立adapter

  return Scaffold(
      appBar: new AppBar(
        title: new Text('列表頁'),
      ),
      body: Container(
        child: ListView.builder(
          itemBuilder: adapter.itemBuilder, //把adapter配置到list
          itemCount: adapter.itemCount,     //
        ),
      ));
}
複製程式碼

1.4 配上假資料

最後我們給 List 頁面配上一些假資料來看看效果。具體過程我們已經學過了

  1. effect 在頁面初始化時建立假資料
  2. effect 把傳送攜帶假資料的 action
  3. reducer 接收 action,通過假資料更新state

大家基本都會了,要注意一點這次 action 攜帶了引數。貼一下程式碼

手把手入門Fish-Redux開發flutter(下)
手把手入門Fish-Redux開發flutter(下)
手把手入門Fish-Redux開發flutter(下)

最後執行一下看看效果

手把手入門Fish-Redux開發flutter(下)

2 使用全域性 store 更換主題

這次我們來接觸一下 store。它負責管理全域性的狀態,我們以主題顏色為例進行演示。

2.1 建立全域性state

之前頁面和元件的建立都是通過外掛模板,store 這次手動建立。 建立名為 store 的 package。然後建立一個 state.dart,我們用它來儲存全域性狀態。本例中就是儲存主題顏色。/store/state.dart 如下

import 'dart:ui';
import 'package:fish_redux/fish_redux.dart';

abstract class GlobalBaseState {
  Color get themeColor;
  set themeColor(Color color);
}

class GlobalState implements GlobalBaseState, Cloneable<GlobalState> {
  @override
  Color themeColor;

  @override
  GlobalState clone() {
    return GlobalState();
  }
}
複製程式碼

注意這裡我們先定義了一個抽象類 GlobalBaseState 包含 themeColor 欄位。一會兒我們會讓所有頁面的 state 繼承它。另外上面我們還寫了一個它的實現類 GlobalState,即全域性的 state。

2.2 建立store

我們再新建一個 store.dart,作為 App的Store。/store/store.dart 如下

import 'package:fish_redux/fish_redux.dart';
import 'state.dart';

class GlobalStore {
  static Store<GlobalState> _globalStore;

  static Store<GlobalState> get store =>
    _globalStore ??= createStore<GlobalState>(GlobalState(), buildReducer());

}
複製程式碼

可以看到 store 儲存了全域性 state,並且它也能擁有 reducer 來處理事件。接下來我們就定義它的 action 和 reducer。

2.3 建立store的action和reducer

建立 action.dart ,定義一個改變主題顏色的事件。/store/action.dart 如下

import 'package:fish_redux/fish_redux.dart';

enum GlobalAction { changeThemeColor }

class GlobalActionCreator {
  static Action onchangeThemeColor() {
    return const Action(GlobalAction.changeThemeColor);
  }
}
複製程式碼

建立 reducer.dart,接收事件並修改 GlobalState 的主題色。(這裡我們讓主題色在藍色和綠色中來回切換)/store/reducer.dart 如下

import 'dart:ui';
import 'package:fish_redux/fish_redux.dart';
import 'package:flutter/material.dart' hide Action;
import 'action.dart';
import 'state.dart';

Reducer<GlobalState> buildReducer() {
  return asReducer(
    <Object, Reducer<GlobalState>>{
      GlobalAction.changeThemeColor: _onchangeThemeColor,
    },
  );
}

GlobalState _onchangeThemeColor(GlobalState state, Action action) {
  final Color color =
  state.themeColor == Colors.green ? Colors.blue : Colors.green;
  return state.clone()..themeColor = color;
}
複製程式碼

這樣整個 store 就寫好了。全家福

手把手入門Fish-Redux開發flutter(下)

2.4 繼承GlobalBaseState

我們讓所有頁面的 state 繼承 GlobalBaseState,並在頁面檢視中吧標題欄的顏色設定為 themeColor。我們一共有三個介面(entrance、grid、list)需要修改,以 list 頁面為例,首先是 /list/state.dart

手把手入門Fish-Redux開發flutter(下)

然後是 /list/view.dart

手把手入門Fish-Redux開發flutter(下)

其他兩個頁面同理,不一一展示了。

2.5 關聯state

如何把各個頁面的 state 和全域性 GlobalState 聯絡起來呢?我們要在 app.dart 中配置 visitor。在這裡,我們判斷頁面是否繼承了 GlobalBaseState,然後跟全域性 store 建立聯絡,即該頁面的 state 隨全域性state更新而更新。修改好的 app.dart如下

Widget createApp() {
  final AbstractRoutes routes = PageRoutes(
    pages: <String, Page<Object, dynamic>>{
      'entrance_page': EntrancePage(),
      'grid_page': GridPage(),
      'list_page': ListPage(),
    },
    visitor: (String path, Page<Object, dynamic> page) {
      /// 滿足條件 Page<T> ,T 是 GlobalBaseState 的子類。
      if (page.isTypeof<GlobalBaseState>()) {
        /// 建立 AppStore 驅動 PageStore 的單向資料連線
        /// 1. 引數1 AppStore
        /// 2. 引數2 當 AppStore.state 變化時, PageStore.state 該如何變化
        page.connectExtraStore<GlobalState>(
          GlobalStore.store, (Object pagestate, GlobalState appState) {
          final GlobalBaseState p = pagestate;
          if (p.themeColor != appState.themeColor) {
            if (pagestate is Cloneable) {
              final Object copy = pagestate.clone();
              final GlobalBaseState newState = copy;
              newState.themeColor = appState.themeColor;
              return newState;
            }
          }
          return pagestate;
        });
      }
    },
  );
//以下省略...
}
複製程式碼

2.6 觸發主題修改

最後我希望通過點選 List 頁面的 item,來實現主題色的切換。

第一步

首先我們定義一個 action,/list/item/action.dart 如下

import 'package:fish_redux/fish_redux.dart';

enum ItemAction { action, onThemeChange }

class ItemActionCreator {
  static Action onAction() {
    return const Action(ItemAction.action);
  }

  static Action onThemeChange() {
    return const Action(ItemAction.onThemeChange);
  }
}
複製程式碼

第二步

我們在 item 點選時,傳送這個事件。/list/item/view.dart 如下


Widget buildView(ItemState state, Dispatch dispatch, ViewService viewService) {
  return Container(
    margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
    height: 120.0,
    color: Colors.white,
    child: GestureDetector(
      child: Row(
        //省略...
      ),
      onTap: () {
        dispatch(ItemActionCreator.onThemeChange());
      },
    ),
  );
}
複製程式碼

第三步

在 effect 接收事件,併傳送我們之前定義的全域性修改主題色事件。/list/item/effect.dart 如下

import 'package:fish_demo/store/action.dart';
import 'package:fish_demo/store/store.dart';
import 'package:fish_redux/fish_redux.dart';
import 'action.dart';
import 'state.dart';

Effect<ItemState> buildEffect() {
  return combineEffects(<Object, Effect<ItemState>>{
    ItemAction.action: _onAction,
    ItemAction.onThemeChange: _onThemeChange,
  });
}

void _onAction(Action action, Context<ItemState> ctx) {
}

void _onThemeChange(Action action, Context<ItemState> ctx) {
  GlobalStore.store.dispatch(GlobalActionCreator.onchangeThemeColor());
}
複製程式碼

最後執行看下效果: 一步步進入列表頁,多次點選item,主題色隨之變化。返回上級頁面,主題色也被改變。

手把手入門Fish-Redux開發flutter(下)

總結

至此,我們瞭解了 fish-redux 的一些基本的使用。由於客戶端同學對 redux 類框架接觸不多,所以在使用時要轉變思路、多看文件、多思考。

專案原始碼 github.com/Jegaming/Fi…

🤗如果我的內容對您有幫助,歡迎點贊、評論、轉發、收藏。