再談 PVState 架構(gòu)模式

Flutter 有眾多的狀態(tài)管理方案,我除了對(duì) Provider 有一些了解外廓推,其它的一概不熟悉,主要還是因?yàn)榻佑| Flutter 的時(shí)間太短。

我目前一直是用的原生的 setState艇炎,它已經(jīng)能很好的滿足我的需要了。并且它足夠簡(jiǎn)單腾窝。我目前基于自己的業(yè)務(wù)場(chǎng)景似乎找不到任何去選用其它狀態(tài)管理庫(kù)的理由缀踪。

我一直認(rèn)為對(duì)于一個(gè)庫(kù)來(lái)講,解決問(wèn)題并保持簡(jiǎn)單好用是最重要的虹脯。如果背離了這個(gè)點(diǎn)驴娃,就很有可能導(dǎo)致過(guò)度設(shè)計(jì)。我見(jiàn)過(guò)一些人把我認(rèn)為的最差實(shí)踐當(dāng)成了最佳實(shí)踐循集,而他們卻渾然不知托慨。

當(dāng)然使用 setState 時(shí),也不是完全的裸用暇榴。我做了相當(dāng)輕量級(jí)的封裝厚棵,只有 110 行代碼。我把它稱為 PVState 架構(gòu)模式蔼紧。它既是一種和傳統(tǒng) MVC婆硬、MVP 平輩的架構(gòu)模式,也是一個(gè)輕量級(jí)的狀態(tài)管理方案奸例。我們來(lái)一探究竟吧彬犯。

PVState 概念介紹

首先我封裝了一個(gè) BaseState,所有的 State 都直接或間接地繼承了它查吊。它的子類分為兩種谐区,P State 和 V State。P State 的全稱是 Presenter State逻卖,它負(fù)責(zé)業(yè)務(wù)邏輯宋列。V State 的全稱是 View State,它負(fù)責(zé) UI评也,主要是重寫(xiě) build 方法建立數(shù)據(jù)和 Widget 的單向綁定關(guān)系炼杖。

它們的關(guān)系如圖所示:

PVState 架構(gòu)模式

這里有一個(gè)很重要的點(diǎn)是 V State 繼承了 P State

原則上來(lái)講盗迟,所有與業(yè)務(wù)邏輯相關(guān)的方法坤邪、變量、表達(dá)式均定義在 P State 中罚缕。所有與 UI 相關(guān)的東西比如約束布局的 id艇纺、顏色、buildXXX 系列方法均定義在 V State 中。由于兩者的繼承關(guān)系黔衡,V State 可以無(wú)縫地訪問(wèn) P State 中定義的各種方法消约、變量、表達(dá)式员帮。

當(dāng)要更新 UI 時(shí)或粮,只需要在 P State 中調(diào)用 setState 即可。業(yè)務(wù)邏輯和 UI 實(shí)現(xiàn)了完全解耦捞高,同時(shí)保持了簡(jiǎn)單氯材。

由于這個(gè)架構(gòu)模式很簡(jiǎn)單,相信你已經(jīng)聽(tīng)懂了硝岗,接下來(lái)我們來(lái)分析一下我封裝的源碼氢哮。

PVState 源碼分析

BaseState 的代碼如下:

abstract class BaseState<T extends StatefulWidget> extends State<T> {
  static List<BasePagePStateMixin> pageStack = [];

  @override
  void initState() {
    super.initState();
    if (this is BasePagePStateMixin) {
      if (pageStack.isNotEmpty) {
        (pageStack.last).onPushNext();
      }
      pageStack.add(this as BasePagePStateMixin);
      scheduleMicrotask(() {
        (this as BasePagePStateMixin).onPush();
      });
    }
  }

  P? find<P extends BasePagePStateMixin>() {
    for (final element in pageStack.reversed) {
      if (element is P) {
        return element;
      }
    }
    return null;
  }

  @override
  void dispose() {
    super.dispose();
    if (this is BasePagePStateMixin) {
      (this as BasePagePStateMixin).onPop();
      int index = pageStack.indexOf(this as BasePagePStateMixin);
      pageStack.removeAt(index);
      if (index == pageStack.length - 1 && pageStack.isNotEmpty) {
        scheduleMicrotask(() {
          pageStack.last.onPopNext();
        });
      }
    }
  }
}

BaseState 目前做了兩件事情,一是使用靜態(tài)變量存儲(chǔ)了所有的頁(yè)面型 State型檀,可以用來(lái)做路由棧監(jiān)聽(tīng)冗尤,起到了 RouteAware 的作用。

什么是頁(yè)面型 State 呢胀溺?這里我把 State 劃分成了三種裂七,分別是頁(yè)面型 State、對(duì)話框型 State 和嵌入型 State仓坞。它們分別對(duì)應(yīng)于整個(gè)頁(yè)面的 StatefulWidget背零、整個(gè)對(duì)話框的 StatefulWidget 和嵌入到頁(yè)面內(nèi)的 StatefulWidget。原則上只有頁(yè)面型 State 才有路由棧監(jiān)聽(tīng)的能力无埃,對(duì)吧徙瓶?

不同型的 State 應(yīng)繼承不同型的 BaseState,源碼如下:

abstract class BasePState<T extends StatefulWidget> extends BaseState<T> {
  @override
  Widget build(BuildContext context) {
    throw Exception('Do not call super.build()');
  }
}

/// For pages
abstract class BasePagePState<T extends StatefulWidget> extends BasePState<T>
    with BasePagePStateMixin {}

/// For dialogs
abstract class BaseDialogPState<T extends StatefulWidget> extends BasePState<T>
    with BaseDialogPStateMixin {}

/// For embedded widgets
abstract class BaseWidgetPState<T extends StatefulWidget> extends BasePState<T>
    with BaseWidgetPStateMixin {}

mixin BasePagePStateMixin {
  void onPush() {}

  void onPop() {}

  void onPushNext() {}

  void onPopNext() {}
}

mixin BaseDialogPStateMixin {}

mixin BaseWidgetPStateMixin {}

如圖所示:

繼承結(jié)構(gòu)

此外嫉称,BaseState 還提供了 find 方法侦镇,用于在路由棧中向前查找 State,比如查找 AppState织阅,進(jìn)行一些全局的操作壳繁。這里可以使用 ValueNotifier 進(jìn)行多頁(yè)面的狀態(tài)共享,我提供了簡(jiǎn)易方法:

ValueNotifier obs<V>(V? initialValue) {
  return ValueNotifier(initialValue);
}

最后蒲稳,我提供了一個(gè)名為 Stateful 的通用 StatefulWidget氮趋,這樣你就無(wú)需再為每一個(gè) State 定義一個(gè) StatefulWidget 了伍派。源碼如下:

class Stateful<T extends BasePState> extends StatefulWidget {
  final T state;
  final Map<String, dynamic>? arguments;

  static Stateful of<S extends BasePState>(
    S newState, {
    Key? key,
    Map<String, dynamic>? arguments,
  }) {
    return Stateful(
      key: key,
      state: newState,
      arguments: arguments,
    );
  }

  const Stateful({
    Key? key,
    required this.state,
    this.arguments,
  }) : super(key: key);

  @override
  State createState() {
    return state;
  }
}

使用方法如下:

void main() {
  runApp(Stateful.of(CounterVState()));
}

它還支持傳參江耀,用法如下:

void main() {
  runApp(Stateful.of(CounterVState(), arguments: {
    'initialCount': 0,
  }));
}
abstract class CounterPState extends BasePagePState<Stateful> {
  int count = 0;

  void add() {
    setState(() {
      count++;
    });
  }

  @override
  void initState() {
    super.initState();
    count = widget.arguments!['initialCount'];
  }
}

可以直接在 initState 中獲取到參數(shù)。這里切記要為 BasePagePState 加上 Stateful 泛型參數(shù)诉植。

計(jì)數(shù)器示例

最后看看用 PVState 架構(gòu)模式實(shí)現(xiàn)的計(jì)數(shù)器的完整源碼吧:

void main() {
  runApp(Stateful.of(CounterVState(), arguments: {
    'initialCount': 0,
  }));
}

abstract class CounterPState extends BasePagePState<Stateful> {
  late int count;

  void add() {
    setState(() {
      count++;
    });
  }

  @override
  void initState() {
    super.initState();
    count = widget.arguments!['initialCount'];
  }
}

class CounterVState extends CounterPState {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter Demo Home Page'),
        ),
        body: ConstraintLayout(
          children: [
            const Text(
              'You have pushed the button this many times:',
            ).applyConstraint(
              centerTo: parent,
            ),
            Text(
              '$count', // Direct access to count
              style: Theme.of(context).textTheme.headline4,
            ).applyConstraint(
              outBottomCenterTo: rId(0),
            ),
            FloatingActionButton(
              onPressed: add, // Widget and logic separation
              child: const Icon(Icons.add),
            ).applyConstraint(
              bottomRightTo: parent.rightMargin(20).bottomMargin(20),
            )
          ],
        ),
      ),
    );
  }
}

結(jié)束語(yǔ)

在我的實(shí)際使用中祥国,我還為 BaseState 封裝了一些其它的能力,比如 showLoading、showToast舌稀、sendRequest 等等啊犬。你也可以結(jié)合實(shí)際情況添加一些東西。架構(gòu)模式的源碼已開(kāi)源到 https://github.com/hackware1993/Flutter_PVState

我是 hackware壁查,關(guān)注我(公眾號(hào):FlutterFirst)觉至,一起成長(zhǎng)!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末睡腿,一起剝皮案震驚了整個(gè)濱河市语御,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌席怪,老刑警劉巖应闯,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異挂捻,居然都是意外死亡碉纺,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門刻撒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)骨田,“玉大人,你說(shuō)我怎么就攤上這事声怔∈⒊牛” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵捧搞,是天一觀的道長(zhǎng)抵卫。 經(jīng)常有香客問(wèn)我,道長(zhǎng)胎撇,這世上最難降的妖魔是什么介粘? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮晚树,結(jié)果婚禮上姻采,老公的妹妹穿的比我還像新娘。我一直安慰自己爵憎,他們只是感情好慨亲,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著宝鼓,像睡著了一般刑棵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上愚铡,一...
    開(kāi)封第一講書(shū)人閱讀 51,688評(píng)論 1 305
  • 那天蛉签,我揣著相機(jī)與錄音胡陪,去河邊找鬼。 笑死碍舍,一個(gè)胖子當(dāng)著我的面吹牛柠座,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播片橡,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼妈经,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了捧书?” 一聲冷哼從身側(cè)響起狂塘,我...
    開(kāi)封第一講書(shū)人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鳄厌,沒(méi)想到半個(gè)月后荞胡,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡了嚎,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年泪漂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片歪泳。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡萝勤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出呐伞,到底是詐尸還是另有隱情敌卓,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布伶氢,位于F島的核電站趟径,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏癣防。R本人自食惡果不足惜蜗巧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蕾盯。 院中可真熱鬧幕屹,春花似錦、人聲如沸级遭。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)挫鸽。三九已至说敏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間掠兄,已是汗流浹背像云。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工锌雀, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蚂夕,地道東北人迅诬。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像婿牍,于是被迫代替她去往敵國(guó)和親侈贷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容