主頁 > 移動端開發 > Flutter 對狀態管理的認知與思考

Flutter 對狀態管理的認知與思考

2021-09-27 09:40:54 移動端開發

前言

編程技術交流圣地[-Flutter群-] 發起的 狀態管理研究小組,將就 狀態管理 相關話題進行為期 兩個月 的討論,

image-20210925202441750

目前只有內定的 5 個人參與討論,如果你對 狀態管理 有什么獨特的見解,或想參與其中,可咨詢 張風捷特烈 ,歡迎和我們共同交流,


關于這篇文章的一些內容,我很久之前就想寫的,但一直沒啥源動力,就一直鴿著

這次被捷特大佬催了幾次,終于把這篇文章寫完了,文章里有我對狀態管理的一些思考和看法,希望能引起茫茫人海中零星的共鳴,,,

image-20210925214350088

image-20210926090647120

image-20210925214254459

狀態管理的認知

變遷

解耦是眾多思想或框架的基石

就拿最最最經典的MVC來說,統一將模塊分為三層

  • Model層:資料管理
  • Controller層:邏輯處理
  • View層:視圖搭建

這個經典的層級劃分能應付很多場景

  • MVP,MVVM也是MVC的變種,本質上都是為了在合適的場景,更合理的解耦

  • 其實這些模式應用在移動端是很合適的,移動端舊時XML的寫法,是獲取其View節點,然后對其節點操作

  • 在JSP的時代,JQuery大行其道,操作DOM節點,重繪資料;如出一轍,

時代總是在發展中前進,技術也在不停變遷;就像普羅米修斯盜火而來,給世間帶來諸多變化

對View節點操作的思想,固定化的套用在如今的前端是不準確的

如今前端是由眾多"狀態"去控制界面展示的,需要用更加精煉的語言去闡述它

包容萬千

狀態管理的重點也就在其表面:狀態和管理

  • 寥寥四字,就精悍的概括了思想及其靈魂

狀態是頁面的靈魂,是業務邏輯和通用邏輯的錨定符,只要分離出狀態,將其管理,就可以將頁面解耦

一般來說,從狀態管理的概念上,可以解耦出多個層級

極簡模式 😃

這是一種十分簡潔的層級劃分,眾多流行的Flutter狀態管理框架,也是如此劃分的,例如:provider,getx

  • view:界面層
  • Logic:邏輯層 + 狀態層

極簡模式

標準模式 🙂

這已經是一種類似MVC的層級劃分了,這種層級也十分常見,例如:cubit(provider和getx也能輕松劃分出這種結構)

  • view:界面
  • Logic:邏輯層
  • State:狀態層

標椎模式

嚴格模式 😐

對于標椎模式而言,已經劃分的很到位了,但還有某一類層次沒有劃分出來:用戶和程式互動的行為

說明下:想要劃分出這一層級,代價必然是很大的,會讓框架的使用復雜度進一步上升

  • 后面分析為什么劃分這一層次,會導致成本很大

常見的狀態管理框架:Bloc,Redux,fish_redux

  • view:界面層
  • Logic:邏輯層
  • State:狀態層
  • Action:行為層

嚴格模式

強迫癥模式 😑

常見的狀態管理框架:Redux,fish_redux

從圖上來看,這個結構已經有點復雜了,為了解耦資料重繪這一層次,付出了巨大的成本

  • view:界面層
  • Logic:邏輯層
  • State:狀態層
  • Action:行為層
  • Reducer:這個層級,是專門用于處理資料變化的

強迫癥模式

思考

對于變化的事物和思想,我們應該去恐懼,去抗拒嗎?

我時常認為:優秀的思想見證變遷,它并不會在時光中衰敗,而是變的越來越璀璨

例如:設計模式

解耦的成本

分離邏輯+狀態層

一個成熟的狀態管理框架,必定將邏輯從界面層里面劃分處理,這是應該一個狀態管理框架的最樸實的初衷

一些看法

實際上,此時付出的成本是針對框架開發者的,需要開發者去選擇一個合適技術方案,去進行合理的解耦

實作一個狀態管理框架,我此時,或許可以說:

  • 這并不是一件多么難的事
  • 幾個檔案就能實作一個合理且功能強大的狀態管理框架

此時,螢屏前的你可能會想了:這叼毛可真會吹牛皮,把👴逗笑了

img

關于上面的話,我真不是吹牛皮,我看了幾個狀態管理的原始碼后,發現狀態管理的思想其實非常樸實,當然開源框架的代碼并沒有那么簡單,基本都做了大量的抽象,方便功能擴展,這基本都會對閱讀者產生極大的困擾,尤其是provider,看的頭皮發麻、、、

我將幾個典型的狀態管理的思想提取出來后,用極簡的代碼復現其運行機制,發現用的都是觀察模式的思想,理解了以后,就并不覺得狀態管理框架多么的神秘了

我絕沒有任何輕視的思想:他們都是那個莽荒時代里,偉大的拓荒者!

如何將邏輯+狀態層從界面里解耦出來?

我總結了幾種很經典的狀態管理的實作機制,因為每一種實作原始碼都有點長,就放在文章后半截了,有興趣的可以看看;每一種實作方式的代碼都是完整的,可獨立運行的

  • 將邏輯層界面解耦出來
    • 成本在框架端,需要較復雜的實作
    • 一般來說,只解耦倆層,使用上一般較為簡單

解耦邏輯層

  • 解耦狀態層
    • 如果分離出邏輯層,解耦狀態層,一般來說,并不會很難;手動簡單劃分即可,我寫的幾個idea插件生成模板代碼,都對該層做了劃分
    • 也可以直接在框架內部直接強行約定,Bloc中的Bloc模式和Cubit模式,redux系列,,,
    • 劃分成本不高,使用成本不高,該層解耦的影響深遠

劃分狀態層

Action層的成本

Action層是什么?正如其名字一樣,行為層,用戶和界面上的互動事件都可以劃分到這一層

  • 例如:點擊按鈕的事件,輸入事件,上拉下拉事件等等
  • 用戶在界面上生成了這些事件,我們也需要做相應的邏輯去回應

為什么要劃分Action層?

  • 大家如果寫flutter套娃代碼寫的很盡興的時候,可能會發現,很多點擊事件的互動入口都在widget山里
  • 互動事件散落在大量的界面代碼,如果需要調整跳轉事件傳參,找起來會很頭痛
  • 還有一個很重要的方面:實際上互動事件的入口,就是業務入口,需求調整時,找相應業務代碼也很麻煩!

基于業務會逐漸鬼畜的考量,一些框架劃分出了Action層,統一管理了所有的互動事件

成本

框架側成本

想要統一管理所有的互動事件,實作上難度不是很大

  • 一般情況下,我們可以直接在view層,直接呼叫邏輯層的方法,執行相關有業務邏輯
  • 現在需要將呼叫邏輯層方法的行為,進行統一的管理
  • 所以,需要在呼叫的中間,增加一個中間層,中轉所有的事件
  • 這個中轉層就是action層,可以管理所有的互動事件

來看下實作思路

Action層劃分思路

框架側實作成本并不高,主要就是對事件的接受和分發

實際上,我們一般也不在乎框架側成本,框架內部實作的再怎么復雜都無關緊要,用法應該簡潔明了

如果內部設計非常精妙,使用起來卻晦澀繁瑣,無疑是給使用者增加心智負擔

使用側成本

劃分出Action層,會給使用者增加一定的使用成本,這是無法避免的

  • 事件定義成本:因為劃分出了事件層,每一種互動,必須在Action層去定義
  • 發送事件成本:在view層需要將定義的事件用不同的api發送出去,這個對比以前呼叫區別不大,成本很低
  • 邏輯層處理成本:邏輯層必定會多一個模塊或方法,接受分發的方法去分類處理,此處會有一點繁瑣

圖中紅框的模塊,是額外的使用成本

Action層使用成本

外在表現

Bloc不使用Action

  • View層,代碼簡寫,只是看看其外在表現
class BlBlocCounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (BuildContext context) => BlBlocCounterBloc()..init(),
      child: Builder(builder: (context) => _buildPage(context)),
    );
  }

  Widget _buildPage(BuildContext context) {
    final bloc = BlocProvider.of<BlBlocCounterBloc>(context);

    return Scaffold(
      ...
      floatingActionButton: FloatingActionButton(
        //呼叫業務方法
        onPressed: () => bloc.increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}
  • Bloc層
class BlBlocCounterBloc extends Bloc<BlBlocCounterEvent, BlBlocCounterState> {
  BlBlocCounterBloc() : super(BlBlocCounterState().init());

  void init() async {
    ///處理邏輯,呼叫emit方法重繪
    emit(state.clone());
  }
    
  ...
}

state層:該演示中,此層不重要,不寫了

Bloc使用Action

  • View層,代碼簡寫,只是看看其外在表現
class BlBlocCounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (BuildContext context) => BlBlocCounterBloc()..add(InitEvent()),
      child: Builder(builder: (context) => _buildPage(context)),
    );
  }

  Widget _buildPage(BuildContext context) {
    final bloc = BlocProvider.of<BlBlocCounterBloc>(context);

    return Scaffold(
      ...
      floatingActionButton: FloatingActionButton(
        onPressed: () => bloc.add(AEvent()),
        child: Icon(Icons.add),
      ),
    );
  }
}
  • Bloc層
class BlBlocCounterBloc extends Bloc<BlBlocCounterEvent, BlBlocCounterState> {
  BlBlocCounterBloc() : super(BlBlocCounterState().init());

  @override
  Stream<BlBlocCounterState> mapEventToState(BlBlocCounterEvent event) async* {
    if (event is InitEvent) {
      yield await init();
    } else if (event is AEvent) {
      yield a();
    } else if (event is BEvent) {
      yield b();
    } else if (event is CEvent) {
      yield c();
    } else if (event is DEvent) {
      yield d();
    } else if (event is EEvent) {
      yield e();
    } else if (event is FEvent) {
      yield f();
    } else if (event is GEvent) {
      yield g();
    } else if (event is HEvent) {
      yield h();
    } else if (event is IEvent) {
      yield i();
    } else if (event is JEvent) {
      yield j();
    } else if (event is KEvent) {
      yield k();
    }
  }

  ///對應業務方法
  ...
}
  • Event層:如果需要傳引數,事件類里面就需要定義相關變數,實作其建構式,將view層資料傳輸到bloc層
abstract class BlBlocCounterEvent {}

class InitEvent extends BlBlocCounterEvent {}

class AEvent extends BlBlocCounterEvent {}

class BEvent extends BlBlocCounterEvent {}

class CEvent extends BlBlocCounterEvent {}

.......

class KEvent extends BlBlocCounterEvent {}

state層:該演示中,此層不重要,不寫了

fish_redux的使用表現

  • view
Widget buildView(MainState state, Dispatch dispatch, ViewService viewService) {
  return Scaffold(
    //頂部AppBar
    appBar: mainAppBar(
      onTap: () => dispatch(MainActionCreator.toSearch()),
    ),
    //側邊抽屜模塊
    drawer: MainDrawer(
      data: state,
      onTap: (String tag) => dispatch(MainActionCreator.clickDrawer(tag)),
    ),
    //頁面主體
    body: MainBody(
      data: state,
      onChanged: (int index) => dispatch(MainActionCreator.selectTab(index)),
    ),
    //底部導航
    bottomNavigationBar: MainBottomNavigation(
      data: state,
      onTap: (int index) => dispatch(MainActionCreator.selectTab(index)),
    ),
  );
}
  • action層
enum MainAction {
  //切換tab
  selectTab,
  //側邊欄item點擊
  clickDrawer,
  //搜索
  toSearch,
  //統一重繪事件
  onRefresh,
}

class MainActionCreator {
  static Action toSearch() {
    return Action(MainAction.toSearch);
  }

  static Action selectTab(int index) {
    return Action(MainAction.selectTab, payload: index);
  }

  static Action onRefresh() {
    return Action(MainAction.onRefresh);
  }

  static Action clickDrawer(String tag) {
    return Action(MainAction.clickDrawer, payload: tag);
  }
}
  • Effect
Effect<MainState> buildEffect() {
  return combineEffects(<Object, Effect<MainState>>{
    //初始化
    Lifecycle.initState: _init,
    //切換tab
    MainAction.selectTab: _selectTab,
    //選擇相應抽屜內部的item
    MainAction.clickDrawer: _clickDrawer,
    //跳轉搜索頁面
    MainAction.toSearch: _toSearch,
  });
}

///眾多業務方法
void _init(Action action, Context<MainState> ctx) async {
  ...
}
  • reducer和state層不重要,這地方就不寫了

fish_redux對Action層的劃分以及事件的分發,明顯要比Bloc老道很多

fish_redux使用列舉和一個類就完成了眾多事件的定義;bloc需要繼承類,一個類一個事件

老實說,倆種框架我都用了,bloc這樣寫確實比較麻煩,尤其涉及傳參的時候,就需要在類里面定義很多變數

總結

上面幾種形式對比,可以發現區別還是蠻大的

增加了Action層,使得使用成本不可避免的飆升

很多人心里,此時或許都會吐槽:好麻煩,,,

img

對Action層的思考和演化

通過對分離Action層的設計本質分析,我們會發現一個無法避免的現實!

  • 增加Action層,使用端的成本無法避免
  • 因為使用端增加的成本,就是框架側的設計核心

Action層使用成本

當業務逐漸的復雜起來,Action層的劃分是勢在必行的,我們必須歸納事件入口;當業務頻繁調整時,需要能快速的定位對應的業務!

有辦法簡化嗎?

Action層的劃分,會一定程度上增加使用者的負擔,有什么辦法可以簡化呢?同時又能達到管理事件入口的效果?

我曾對View層瘋狂套娃的Widget,做了很多思考,對拆分形式做了一些嘗試

拆分后的效果,將View層和Action很好的結合起來了,具體操作:Flutter 改善套娃地獄問題(仿喜馬拉雅PC頁面舉例)

  • 看下拆分后的代碼效果
    • 因為將View分模塊劃分清晰了,對外暴露方法就是業務事件,可以很輕松的定位到對應的業務了
    • 如此形式劃分后,對應的頁面結構也變得例外清晰,修改頁面對應的模塊也很輕松了

carbon

  • 對View層進行相關改造后
    • 可以非常方便的定位業務和界面模塊
    • 同時也避免的Action層一系列稍顯繁瑣的操作

Action的演變

總結

框架的約定,可以規范眾多行為習慣不同的開發者

后面我提出的對View層的拆分,只能依靠開發者本身的意識

這里,我給出一種不一樣的方式,其中的取舍,只能由各位自己決定嘍

我目前一直都是使用View層的拆分,自我感覺對后期復雜模塊的維護,非常友好~~

Reducer層的吐槽

可能是我太菜了,一直感受不到這一層分化的妙處

我用fish_redux也寫了很多頁面(用了一年了),之前也會將相關資料通過Action層傳遞到Reducer,然后進行相應的重繪,這導致了一個問題!

  • 我每次重繪不同行為的資料,就需要創建一個Action
  • 然后在Reducer層決議傳過來的資料,再往clone物件里賦值,導致我想修改資料的時候,必須先要去Effect層去看邏輯,然后去Reducer里面修改賦值
  • 來回跳,麻煩到爆!

被繞了多次,煩躁了多次后,我直接把Reducer層寫成了一個重繪方法!

Reducer<WebViewState> buildReducer() {
  return asReducer(
    <Object, Reducer<WebViewState>>{
      WebViewAction.onRefresh: _onRefresh,
    },
  );
}

WebViewState _onRefresh(WebViewState state, Action action) {
  return state.clone();
}

就算在復雜的模塊,我也沒感受到他給我帶來的好處,我就只能把他無限榷訓成一個重繪方法了

img

狀態管理的幾種實作

這是我看了一些狀態管理的原始碼

  • 總結出的幾種狀態管理的重繪機制
  • 任選一種,都可以搓出你自己的狀態管理框架

之前的幾篇原始碼剖析文章寫過,整理了下,做個總結

img

爛大街的實作

實作難度最小 ?

這是一種非常常見的實作

  • 這是一種簡單,易用,強大的實作
  • 同時由于難度不高,也是一種爛大街的實作

實作

需要實作一個管理邏輯層實體的的中間件:依賴注入的實作

也可以使用InheritedWidget保存和傳遞邏輯層實體(Bloc就是這樣做的);但是自己管理,可以大大拓寬使用場景,此處就自己實作一個管理實體的中間件

  • 這邊只實作三個基礎api
///依賴注入,外部可將實體,注入該類中,由該類管理
class Easy {
  ///注入實體
  static T put<T>(T dependency, {String? tag}) =>
      _EasyInstance().put(dependency, tag: tag);

  ///獲取注入的實體
  static T find<T>({String? tag, String? key}) =>
      _EasyInstance().find<T>(tag: tag, key: key);

  ///洗掉實體
  static bool delete<T>({String? tag, String? key}) =>
      _EasyInstance().delete<T>(tag: tag, key: key);
}

///具體邏輯
class _EasyInstance {
  factory _EasyInstance() => _instance ??= _EasyInstance._();

  static _EasyInstance? _instance;

  _EasyInstance._();

  static final Map<String, _InstanceInfo> _single = {};

  ///注入實體
  T put<T>(T dependency, {String? tag}) {
    final key = _getKey(T, tag);
    //只保存第一次注入:針對自動重繪機制優化,每次熱多載的時候,資料不會重置
    _single.putIfAbsent(key, () => _InstanceInfo<T>(dependency));
    return find<T>(tag: tag);
  }

  ///獲取注入的實體
  T find<T>({String? tag, String? key}) {
    final newKey = key ?? _getKey(T, tag);
    var info = _single[newKey];

    if (info?.value != null) {
      return info!.value;
    } else {
      throw '"$T" not found. You need to call "Easy.put($T())""';
    }
  }

  ///洗掉實體
  bool delete<T>({String? tag, String? key}) {
    final newKey = key ?? _getKey(T, tag);
    if (!_single.containsKey(newKey)) {
      print('Instance "$newKey" already removed.');
      return false;
    }

    _single.remove(newKey);
    print('Instance "$newKey" deleted.');
    return true;
  }

  String _getKey(Type type, String? name) {
    return name == null ? type.toString() : type.toString() + name;
  }
}

class _InstanceInfo<T> {
  _InstanceInfo(this.value);
  T value;
}

定義一個監聽和基類

  • 也可以使用ChangeNotifier;此處我們自己簡單定義個
class EasyXNotifier {
  List<VoidCallback> _listeners = [];

  void addListener(VoidCallback listener) => _listeners.add(listener);

  void removeListener(VoidCallback listener) {
    for (final entry in _listeners) {
      if (entry == listener) {
        _listeners.remove(entry);
        return;
      }
    }
  }

  void dispose() => _listeners.clear();

  void notify() {
    if (_listeners.isEmpty) return;

    for (final entry in _listeners) {
      entry.call();
    }
  }
}
  • 我這地方寫的極簡,相關生命周期都沒加,為了代碼簡潔,這個暫且不表
class EasyXController {
  EasyXNotifier xNotifier = EasyXNotifier();

  ///重繪控制元件
  void update() => xNotifier.notify();
}

再來看看最核心的EasyBuilder控制元件:這就搞定了!

  • 實作代碼寫的極其簡單,希望大家思路能有所明晰
///重繪控制元件,自帶回識訓制
class EasyBuilder<T extends EasyXController> extends StatefulWidget {
  final Widget Function(T logic) builder;
  final String? tag;
  final bool autoRemove;

  const EasyBuilder({
    Key? key,
    required this.builder,
    this.autoRemove = true,
    this.tag,
  }) : super(key: key);

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

class _EasyBuilderState<T extends EasyXController> extends State<EasyBuilder<T>> {
  late T controller;

  @override
  void initState() {
    super.initState();
	
    ///此處是整個類的靈魂代碼
    controller = Easy.find<T>(tag: widget.tag);
    controller.xNotifier.addListener(() {
      if (mounted) setState(() {});
    });
  }

  @override
  void dispose() {
    if (widget.autoRemove) {
      Easy.delete<T>(tag: widget.tag);
    }
    controller.xNotifier.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) => widget.builder(controller);
}

使用

  • 使用很簡單,先看下邏輯層
class EasyXCounterLogic extends EasyXController {
  var count = 0;

  void increase() {
    ++count;
    update();
  }
}
  • 界面層
class EasyXCounterPage extends StatelessWidget {
  final logic = Easy.put(EasyXCounterLogic());

  @override
  Widget build(BuildContext context) {
    return BaseScaffold(
      appBar: AppBar(title: const Text('EasyX-自定義EasyBuilder重繪機制')),
      body: Center(
        child: EasyBuilder<EasyXCounterLogic>(builder: (logic) {
          return Text(
            '點擊了 ${logic.count} 次',
            style: TextStyle(fontSize: 30.0),
          );
        }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => logic.increase(),
        child: Icon(Icons.add),
      ),
    );
  }
}
  • 效果圖

easy_x_builder

InheritedWidget的實作

實作具有一定的難度 ??

更加詳細的決議可查看:Flutter Provider的另一面

先來看下InheritedWidget它自帶一些功能

  • 儲存資料,且資料可以隨著父子節點傳遞
  • 自帶區域重繪機制

資料傳遞

InheritedWidget存取資料

區域重繪

InheritedWidget對子節點的Element,有個強大的操作功能

  • 可以將子widget的element實體,儲存在自身的InheritedElement中的_dependents變數中
  • 呼叫其notifyClients方法,會遍歷_dependents中的子Element,然后呼叫子Element的markNeedsBuild方法,就完成了定點重繪子節點的操作

InheritedWIdget重繪機制

有了上面這倆個關鍵知識,就可以輕松的實作一個強大的狀態管理框架了,來看下實作

實作

  • ChangeNotifierEasyP:類比Provider的ChangeNotifierProvider
class ChangeNotifierEasyP<T extends ChangeNotifier> extends StatelessWidget {
  ChangeNotifierEasyP({
    Key? key,
    required this.create,
    this.builder,
    this.child,
  }) : super(key: key);

  final T Function(BuildContext context) create;

  final Widget Function(BuildContext context)? builder;
  final Widget? child;

  @override
  Widget build(BuildContext context) {
    assert(
      builder != null || child != null,
      '$runtimeType  must specify a child',
    );

    return EasyPInherited(
      create: create,
      child: builder != null
          ? Builder(builder: (context) => builder!(context))
          : child!,
    );
  }
}

class EasyPInherited<T extends ChangeNotifier> extends InheritedWidget {
  EasyPInherited({
    Key? key,
    required Widget child,
    required this.create,
  }) : super(key: key, child: child);

  final T Function(BuildContext context) create;

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => false;

  @override
  InheritedElement createElement() => EasyPInheritedElement(this);
}

class EasyPInheritedElement<T extends ChangeNotifier> extends InheritedElement {
  EasyPInheritedElement(EasyPInherited<T> widget) : super(widget);

  bool _firstBuild = true;
  bool _shouldNotify = false;
  late T _value;
  late void Function() _callBack;

  T get value => _value;

  @override
  void performRebuild() {
    if (_firstBuild) {
      _firstBuild = false;
      _value = (widget as EasyPInherited<T>).create(this);

      _value.addListener(_callBack = () {
        // 處理重繪邏輯,此處無法直接呼叫notifyClients
        // 會導致owner!._debugCurrentBuildTarget為null,觸發斷言條件,無法向后執行
        _shouldNotify = true;
        markNeedsBuild();
      });
    }

    super.performRebuild();
  }

  @override
  Widget build() {
    if (_shouldNotify) {
      _shouldNotify = false;
      notifyClients(widget);
    }
    return super.build();
  }

  @override
  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    //此處就直接重繪添加的監聽子Element了,不各種super了
    dependent.markNeedsBuild();
    // super.notifyDependent(oldWidget, dependent);
  }

  @override
  void unmount() {
    _value.removeListener(_callBack);
    _value.dispose();
    super.unmount();
  }
}
  • EasyP:類比Provider的Provider類
class EasyP {
  /// 獲取EasyP實體
  /// 獲取實體的時候,listener引數老是寫錯,這邊直接用倆個方法區分了
  static T of<T extends ChangeNotifier>(BuildContext context) {
    return _getInheritedElement<T>(context).value;
  }

  /// 注冊監聽控制元件
  static T register<T extends ChangeNotifier>(BuildContext context) {
    var element = _getInheritedElement<T>(context);
    context.dependOnInheritedElement(element);
    return element.value;
  }

  /// 獲取距離當前Element最近繼承InheritedElement<T>的組件
  static EasyPInheritedElement<T>
      _getInheritedElement<T extends ChangeNotifier>(BuildContext context) {
    var inheritedElement = context
            .getElementForInheritedWidgetOfExactType<EasyPInherited<T>>()
        as EasyPInheritedElement<T>?;

    if (inheritedElement == null) {
      throw EasyPNotFoundException(T);
    }

    return inheritedElement;
  }
}

class EasyPNotFoundException implements Exception {
  EasyPNotFoundException(this.valueType);

  final Type valueType;

  @override
  String toString() => 'Error: Could not find the EasyP<$valueType>';
}
  • build:最后整一個Build類就行了
class EasyPBuilder<T extends ChangeNotifier> extends StatelessWidget {
  const EasyPBuilder(
    this.builder, {
    Key? key,
  }) : super(key: key);

  final Widget Function() builder;

  @override
  Widget build(BuildContext context) {
    EasyP.register<T>(context);
    return builder();
  }
}

大功告成,上面這三個類,就基于InheritedWidget自帶的功能,實作了一套狀態管理框架

  • 實作了區域重繪功能
  • 實作了邏輯層實體,可以隨著Widget父子節點傳遞功能

使用

用法基本和Provider一摸一樣…

  • view
class CounterEasyPPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierEasyP(
      create: (BuildContext context) => CounterEasyP(),
      builder: (context) => _buildPage(context),
    );
  }

  Widget _buildPage(BuildContext context) {
    final easyP = EasyP.of<CounterEasyP>(context);

    return Scaffold(
      appBar: AppBar(title: Text('自定義狀態管理框架-EasyP范例')),
      body: Center(
        child: EasyPBuilder<CounterEasyP>(() {
          return Text(
            '點擊了 ${easyP.count} 次',
            style: TextStyle(fontSize: 30.0),
          );
        }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => easyP.increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}
  • easyP
class CounterEasyP extends ChangeNotifier {
  int count = 0;

  void increment() {
    count++;
    notifyListeners();
  }
}
  • 效果圖:

easy_p

自動化重繪的實作

實作需要一些的靈感 ???

自動化重繪的實作

  • 將單個狀態變數和重繪組件,建立起了連接
  • 一但變數數值改變,重繪組件自動重繪
  • 某狀態變化,只會自動觸發其重繪組件,其它重繪組件并不觸發

實作

同樣的,需要管理其邏輯類的中間件;為了范例完整,再寫下這個依賴管理類

///依賴注入,外部可將實體,注入該類中,由該類管理
class Easy {
  ///注入實體
  static T put<T>(T dependency, {String? tag}) =>
      _EasyInstance().put(dependency, tag: tag);

  ///獲取注入的實體
  static T find<T>({String? tag, String? key}) =>
      _EasyInstance().find<T>(tag: tag, key: key);

  ///洗掉實體
  static bool delete<T>({String? tag, String? key}) =>
      _EasyInstance().delete<T>(tag: tag, key: key);
}

///具體邏輯
class _EasyInstance {
  factory _EasyInstance() => _instance ??= _EasyInstance._();

  static _EasyInstance? _instance;

  _EasyInstance._();

  static final Map<String, _InstanceInfo> _single = {};

  ///注入實體
  T put<T>(T dependency, {String? tag}) {
    final key = _getKey(T, tag);
    //只保存第一次注入:針對自動重繪機制優化,每次熱多載的時候,資料不會重置
    _single.putIfAbsent(key, () => _InstanceInfo<T>(dependency));
    return find<T>(tag: tag);
  }

  ///獲取注入的實體
  T find<T>({String? tag, String? key}) {
    final newKey = key ?? _getKey(T, tag);
    var info = _single[newKey];

    if (info?.value != null) {
      return info!.value;
    } else {
      throw '"$T" not found. You need to call "Easy.put($T())""';
    }
  }

  ///洗掉實體
  bool delete<T>({String? tag, String? key}) {
    final newKey = key ?? _getKey(T, tag);
    if (!_single.containsKey(newKey)) {
      print('Instance "$newKey" already removed.');
      return false;
    }

    _single.remove(newKey);
    print('Instance "$newKey" deleted.');
    return true;
  }

  String _getKey(Type type, String? name) {
    return name == null ? type.toString() : type.toString() + name;
  }
}

class _InstanceInfo<T> {
  _InstanceInfo(this.value);
  T value;
}
  • 自定義一個監聽類
class EasyXNotifier {
  List<VoidCallback> _listeners = [];

  void addListener(VoidCallback listener) => _listeners.add(listener);

  void removeListener(VoidCallback listener) {
    for (final entry in _listeners) {
      if (entry == listener) {
        _listeners.remove(entry);
        return;
      }
    }
  }

  void dispose() => _listeners.clear();

  void notify() {
    if (_listeners.isEmpty) return;

    for (final entry in _listeners) {
      entry.call();
    }
  }
}

在自動重繪的機制中,需要將基礎型別進行封裝

  • 主要邏輯在Rx中
  • set value 和 get value是關鍵
///拓展函式
extension IntExtension on int {
  RxInt get ebs => RxInt(this);
}

extension StringExtension on String {
  RxString get ebs => RxString(this);
}

extension DoubleExtension on double {
  RxDouble get ebs => RxDouble(this);
}

extension BoolExtension on bool {
  RxBool get ebs => RxBool(this);
}

///封裝各型別
class RxInt extends Rx<int> {
  RxInt(int initial) : super(initial);

  RxInt operator +(int other) {
    value = value + other;
    return this;
  }

  RxInt operator -(int other) {
    value = value - other;
    return this;
  }
}

class RxDouble extends Rx<double> {
  RxDouble(double initial) : super(initial);

  RxDouble operator +(double other) {
    value = value + other;
    return this;
  }

  RxDouble operator -(double other) {
    value = value - other;
    return this;
  }
}

class RxString extends Rx<String> {
  RxString(String initial) : super(initial);
}

class RxBool extends Rx<bool> {
  RxBool(bool initial) : super(initial);
}

///主體邏輯
class Rx<T> {
  EasyXNotifier subject = EasyXNotifier();

  Rx(T initial) {
    _value = initial;
  }

  late T _value;

  bool firstRebuild = true;

  String get string => value.toString();

  @override
  String toString() => value.toString();

  set value(T val) {
    if (_value == val && !firstRebuild) return;
    firstRebuild = false;
    _value = val;

    subject.notify();
  }

  T get value {
    if (RxEasy.proxy != null) {
      RxEasy.proxy!.addListener(subject);
    }
    return _value;
  }
}

需要寫一個非常重要的中轉類,這個也會儲存回應式變數的監聽物件

  • 這個類有著非常核心的邏輯:他將回應式變數和重繪控制元件關聯起來了!
class RxEasy {
  EasyXNotifier easyXNotifier = EasyXNotifier();

  Map<EasyXNotifier, String> _listenerMap = {};

  bool get canUpdate => _listenerMap.isNotEmpty;

  static RxEasy? proxy;

  void addListener(EasyXNotifier notifier) {
    if (!_listenerMap.containsKey(notifier)) {
      //變數監聽中重繪
      notifier.addListener(() {
        //重繪ebx中添加的監聽
        easyXNotifier.notify();
      });
      //添加進入map中
      _listenerMap[notifier] = '';
    }
  }
}

重繪控制元件Ebx

typedef WidgetCallback = Widget Function();

class Ebx extends StatefulWidget {
  const Ebx(this.builder, {Key? key}) : super(key: key);

  final WidgetCallback builder;

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

class _EbxState extends State<Ebx> {
  RxEasy _rxEasy = RxEasy();

  @override
  void initState() {
    super.initState();

    _rxEasy.easyXNotifier.addListener(() {
      if (mounted) setState(() {});
    });
  }

  Widget get notifyChild {
    final observer = RxEasy.proxy;
    RxEasy.proxy = _rxEasy;
    final result = widget.builder();
    if (!_rxEasy.canUpdate) {
      throw 'Widget lacks Rx type variables';
    }
    RxEasy.proxy = observer;
    return result;
  }

  @override
  Widget build(BuildContext context) {
    return notifyChild;
  }

  @override
  void dispose() {
    _rxEasy.easyXNotifier.dispose();

    super.dispose();
  }
}

在自動重繪機制中,回收依賴實體需要針對處理

此處我寫了一個回收控制元件,可以完成實體的自動回收

  • 命名的含義,將實體和控制元件系結,控制元件被回收時,邏輯層實體也將被自動回收
class EasyBindWidget extends StatefulWidget {
  const EasyBindWidget({
    Key? key,
    this.bind,
    this.tag,
    this.binds,
    this.tags,
    required this.child,
  })  : assert(
          binds == null || tags == null || binds.length == tags.length,
          'The binds and tags arrays length should be equal\n'
          'and the elements in the two arrays correspond one-to-one',
        ),
        super(key: key);

  final Object? bind;
  final String? tag;

  final List<Object>? binds;
  final List<String>? tags;

  final Widget child;

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

class _EasyBindWidgetState extends State<EasyBindWidget> {
  @override
  Widget build(BuildContext context) {
    return widget.child;
  }

  @override
  void dispose() {
    _closeController();
    _closeControllers();

    super.dispose();
  }

  void _closeController() {
    if (widget.bind == null) {
      return;
    }

    var key = widget.bind.runtimeType.toString() + (widget.tag ?? '');
    Easy.delete(key: key);
  }

  void _closeControllers() {
    if (widget.binds == null) {
      return;
    }

    for (var i = 0; i < widget.binds!.length; i++) {
      var type = widget.binds![i].runtimeType.toString();

      if (widget.tags == null) {
        Easy.delete(key: type);
      } else {
        var key = type + (widget.tags?[i] ?? '');
        Easy.delete(key: key);
      }
    }
  }
}

使用

  • 邏輯層
class EasyXEbxCounterLogic {
  RxInt count = 0.ebs;

  ///自增
  void increase() => ++count;
}
  • 界面層:頁面頂節點套了一個EasyBindWidget,可以保證依賴注入實體可以自動回收
class EasyXEbxCounterPage extends StatelessWidget {
  final logic = Easy.put(EasyXEbxCounterLogic());

  @override
  Widget build(BuildContext context) {
    return EasyBindWidget(
      bind: logic,
      child: BaseScaffold(
        appBar: AppBar(title: const Text('EasyX-自定義Ebx重繪機制')),
        body: Center(
          child: Ebx(() {
            return Text(
              '點擊了 ${logic.count.value} 次',
              style: TextStyle(fontSize: 30.0),
            );
          }),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => logic.increase(),
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}
  • 效果圖

easy_x_ebx

最后

本文總體上,對狀態管理的各個層次劃分做了一些思考和一點個人的見解,文章后半截也給出了一些狀態管理的實作方案

文章里的內容對想設計狀態管理的靚仔,應該有一些幫助;如果你有相關不同的意見,歡迎在評論區討論

img

相關地址

  • 文章demo地址:flutter_use

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/303338.html

標籤:其他

上一篇:Android Studio Arctic Fox | 2020.3.1、Gradle 7.0升級記錄

下一篇:Android Studio 詳細的插件與Gradle的關系(建議收藏)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more