【Flutter 知識集錦】從 restorationId 來說臨時狀態儲存

張風捷特烈發表於2021-07-21

1、緣起

如果我不提 restorationId 屬性,可能絕大多數人都不知道他是幹嘛的,甚至連它的存在都不知道。即便它在元件作為參中出現的頻率挺高。下面先看一下有該屬性的一些元件,比如:在 ListView 中有 restorationId 的屬性。


GridView 中也有 restorationId 的屬性。


PageView 元件中也有 restorationId 的屬性。


SingleChildScrollView 元件中也有 restorationId 的屬性。


NestedScrollView 元件中也有 restorationId 的屬性。


CustomScrollView 元件中也有 restorationId 的屬性。


TextField 元件中也有一個 restorationId 的屬性。

除此之外還有很多其他的元件有 restorationId 屬性,可以感覺到只要和 滑動沾點邊的,好像都有 restorationId 的屬性。說了這麼多,下面我們先來看一下這個屬性的作用。


2. restorationId 屬性的作用

下面以 ListView 為例,介紹一下 restorationId 屬性的作用。如下兩個動圖分別是 無 restorationId有 restorationId 的效果。可見 restorationId 的作用是在某種情況下,保持滑動的偏移量

無 restorationId有 restorationId
class ListViewDemo extends StatelessWidget {
  const ListViewDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView(
      restorationId: 'toly', //tag1
        children: List.generate(25, (index) => ItemBox(index: index,)).toList());
  }
}

class ItemBox extends StatelessWidget {
  final int index;

  const ItemBox({Key? key, required this.index}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      decoration: BoxDecoration(
          border: Border(
              bottom: BorderSide(
        width: 1 / window.devicePixelRatio,
      ))),
      height: 56,
      child: Text(
        '第 $index 個',
        style: TextStyle(fontSize: 20),
      ),
    );
  }
}
複製程式碼

另外,說明一點,為例方便演示恢復的觸發,需要在 開發者選項 中勾選 不保留活動 ,其作用是使用者離開後會殺掉 Activity 。比如點選Home鍵、選單欄切換介面時,Activity 並不為立即銷燬,而是系統視情況而定。開啟這個選項可以避免測試的不確定因素。注意:測試後,一定要關掉

在 Android 中,是通過 onSaveInstanceState 進行實現的。 當系統"未經你許可" 時銷燬了你的 Activity 時,比如橫豎屏切換、點選 Home 鍵、導航選單欄切換。系統會提供一個機會讓通過 onSaveInstanceState 回撥來你儲存臨時狀態資料,這樣可以保證下次使用者進入時產生違和感
另外有一點非常重要,這裡並不是將狀態永久儲存,當使用者主動退出應用,是不會觸發 onSaveInstanceState 的。也就是說,如果你一個 ListView 設定了 restorationId ,使用者滑了一下後,按返回鍵退出,那麼再進來時不會還原到原位置。注意,要是其生效需要在 MaterialApp 中為 restorationScopeId 指定任意字串。


3.如何通過 restoration 機制儲存其他資料

到這裡可能很多人就已滿足了,原來 restorationId 可以儲存臨時狀態,新技能 get 。但這只是冰山一角, restorationId 是被封裝在 ListView 中,只能儲存滑動偏移量,這還有值得舉一反三,繼續深挖的東西。下面通過官方給的一個計時器小demo,認識一下 RestorationMixin

普通計數器狀態儲存計數器

上面兩個動態表現出通過 狀態儲存 的計時器可以在使用者主動退出應用時,儲存狀態資料,進入時保持狀態。其中的關鍵在於 RestorationMixin 。普通的計時器原始碼就不貼了,大家應該已經爛熟於心了。實現定義一個 RestorableCounter 元件用於介面展示,

void main() => runApp(const RestorationExampleApp());

class RestorationExampleApp extends StatelessWidget {
  const RestorationExampleApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      restorationScopeId: 'app',
      title: 'Restorable Counter',
      home: RestorableCounter(restorationId: 'counter'),
    );
  }
}

class RestorableCounter extends StatefulWidget {
  const RestorableCounter({Key? key, this.restorationId}) : super(key: key);
  final String? restorationId;
  @override
  State<RestorableCounter> createState() => _RestorableCounterState();
}
複製程式碼

如下在 _RestorableCounterState 中進行操作:首先混入 RestorationMixin ,然後覆寫 restorationIdrestoreState 方法。提供 RestorableInt 物件記錄數值 。

class _RestorableCounterState extends State<RestorableCounter>
    with RestorationMixin{ // 1. 混入 RestorationMixin

  // 3. 使用 RestorableInt 物件記錄數值
  final RestorableInt _counter = RestorableInt(0);


  // 2. 覆寫 restorationId 提供 id 
  // @override
  String? get restorationId => widget.restorationId;


  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    // 4. 註冊 _counter
    registerForRestoration(_counter, 'count');
  }


  @override
  void dispose() {
    _counter.dispose(); // 5. 銷燬
    super.dispose();
  }
複製程式碼

在元件構建中,我們可以通過 _counter.value訪問或運算元值。

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('Restorable Counter'),
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          const Text('You have pushed the button this many times:'),
          Text(
            '${_counter.value}',
            style: Theme.of(context).textTheme.headline4,
          ),
        ],
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: _incrementCounter,
      tooltip: 'Increment',
      child: const Icon(Icons.add),
    ),
  );
}

void _incrementCounter() {
  setState(() {
    _counter.value++;
  });
}
複製程式碼

剛才有的是 RestorableInt,可能有人擔心別的資料型別怎麼辦。Flutter 中提供了很多 RestorableXXX 的資料型別以供使用。如果不夠用,可以通過擴充 RestorableProperty<T> 來自定義 RestorableXXX 完成需求。

從官方的更新公告上可以看出,目前暫不支援 iOS ,不過在以後會進行支援。


4. 滑動體系中的狀態儲存是如何實現的

當看完上面的小 demo,你可能會比較好奇,滑動體系中是如何儲存的,下面我們就來看看吧。我們追隨 ListViewrestorationId 屬性蹤跡,可以看到它會一路向父級構造中傳遞。最終在 ScrollView 中作為 Scrollable 元件的入參使用。
也就是說,這個屬性的根源是用於 Scrollable 中的。而這個元件是滑動觸發的根基,這也是為什麼滑動相關的元件都有 restorationId 屬性的原因。

ListView --> BoxScrollView --> ScrollView --> Scrollable
複製程式碼


ScrollableState 混入了 RestorationMixin ,其中用於儲存的型別為 _RestorableScrollOffset

同樣覆寫了 restoreStaterestorationId 方法。


這時再看 TextField 元件的實現也是類似,也就說明 TextField 元件也具有這種恢復狀態的特性。

那本文就到這裡,更深層的 RestorationMixin 實現,以及其相關的其他類,還待繼續研究,敬請期待。

相關文章