重磅! flutter視圖局部更新


新建一個flutter工程, 以flutter框架給我們自動生成的代碼為例, 當我們點擊按鈕更新記數_counter時,最終是通過調用State<T>.setState來更新視圖的:

setState(() {
  _counter++;
})

首先需要理解為什么要setState, 它表示當前節點的數據變更,通知視圖需要更新.更新哪個視圖? 持有當前這個State實例的節點對應的視圖. 注意這個節點具體指的是Element對象, Widget只是創建State實例( _MyHomePageState createState()),並沒有持有, 同樣State又繼續創建了子視圖,也沒有持有子視圖(Widget build(BuildContext context)), 持有State的只有Element. setState的參數是一個方法執行體, 實現哪些數據的具體變更, 所以其實沒有設置所謂的狀態, 還不如叫notifyChanges來的明晰.

其次需要理解視圖如何更新. 像Text那個控件, 文本是作為構造函數的參數直接傳給控件的, 根本連類似setText的方法也沒有! 所以顯示出來的數據要更新除了新建視圖對象外沒有別的辦法!

這里就體現了flutter與傳統移動端界面開發的巨大不同: 視圖是通過新建視圖對象來完成更新的. 以往的界面開發中視圖對象都是一個比較重比較大的對象, 視圖要避免冗余, 要盡量復用, 不要頻繁創建. 但在flutter中就不是這樣了, 代表視圖對象的Widget是輕量對象, 它不持有State, 也不持有Widget, 所有視圖對象都是通過build這種創建型關系建立. 所以開發過程中也要堅決避免自定義的Widget持有數據, 因為Widget對象會被很快替換掉.

有了上述兩點就能明白setState之后發生了什么: 當前_MyHomePageStateWidget build(BuildContext context)方法會被調用, 於是生成了新的Scaffold對象,連帶着AppBar,FloatingActionButton,Column一干控件其中自然包括我們需要展示的Text對象, 這時傳入的文本是更新過后的_counter,於是視圖得以更新.

只是想更新一個個小小的文本框就不得不重新創建整個視圖?!
對, 目前的機制就是這樣. 那隨着視圖層次加深, 界面交互復雜,這種重新創建型操作就沒有一點問題? 畢竟對象再小也有開銷, 那么多對象累積起來,也可能造成創建過程的消耗.於是我們的問題終於來了:
有沒有方法可以只更新部分視圖?

縮小一下更新范圍不就得了? 現在的更新范圍大是因為_MyHomePageState.build被調用返回了整個視圖, 而_MyHomePageState對應的視圖是MyHomePage. 所以創建一個State<Text>, build返回Text控件實例, 再將這個State<Text>持有, 數據變更時調用State<Text>.setState()`不就可以達到目的?

這個想法符合flutter本身的機制, 但問題就是誰來創建這個State<Text>? 如前文所述, 首先只有StatefulWidget才能創建State實例, 其次必須是父節點創建這個State<Text>. 但示例中Text的父節點Column首先就不是StatefulWidget; 就算是了, 我們還要聲明Widget類繼承Column覆蓋build方法, 再聲明State類繼承State<Text>, 煩都煩死了. 那如果從Text向上找一個StatefulWidget, 創建的時候是Text的一個祖先節點, 存在一點冗余可以接受呢? 這個想法實踐上一點也不可行, 且不說有個特定視圖對象的查找過程, 上面所說的各種類聲明一點也沒有減少, 所以這個路子是沒法搞的.

所以還是從setState源碼入手, 看一個節點到底是如何更新視圖的.

State.setState
  Element.markNeedsBuild
    Element._dirty = true;
    BuildOwner.scheduleBuildFor
      BuildOwner._dirtyElements.add
      Element._inDirtyList = true;

過程比想象的簡單, 最后僅僅是將Element節點標識成dirty並加入到了BuildOwner的_dirtyElements列表里. 從Element角度看setState這個名稱似乎也沒有錯, 不過它是相對Element說的, 具體設置的是Elementdirty狀態. 那我們只需找到Text對應的Element節點並調用一下它的markNeedsBuild不就ok了? 所以先要找到Text這個Widget節點對應的Element節點.

在以前的建樹流程中說過Element節點結構像掛鈎, 只有parent沒有直接持有children, 要找子節點需要像Element.visitChildren那樣傳遞一個訪問者來進行遍歷, 而判斷條件自然就是Element持有的Widget是否是我們需要更新的Widget, 於是有:

  static Element findChild(Element e, Widget w) {
    Element child;
    void visit(Element element) {
      if (w == element.widget)
        child = element;
      else
        element.visitChildren(visit);
    }
    visit(e);
    return child;
  }

但是對找到的element設置markNeedsBuild竟然不起作用! 查了半天原因, 才明白還是把建樹流程搞混了, markNeedsBuild僅讓當前Element節點的build被調用, 創建的是當前節點的子節點視圖對象, 而我們現在需要的是把當前子節點持有的視圖對象替換掉('視圖更新是通過創建新的Widget對象'), 同時不能重新創建當前Element節點及其子節點. 而Element.update(Widget)正是這個作用!! 如果說inflateWidget是初始化Element節點樹, 那update正是在樹建立成功后進行更新操作. 於是有

onPressed: () {
  _counter++;
  Element e = findChild(context as Element, title);
  if (e != null) {
    e.update(title);
  }
},

因為要找節點, 所以用了一個title持有了Text, 以方便在onTap()的上下文中作查找參數.
但這樣也是不對的! 這里存在2個問題:

  1. 視圖對象沒有更新. 我們需要展示的是一個新的_counter相關的文本, 因此需要的是一個新的視圖對象, 現在傳入的還是老的視圖對象,等於什么也沒更新...
  2. 直接調用Element.update是有異常的, 跟蹤了一下發現一個標識狀態的數據_debugStateLockLevel不對, 原來要在BuildOwner.lockState中執行才可以.

這里啰里八嗦的寫這一坨是想表明一個的新想法的實現是環環相扣關聯細節的, 很多時候思路是對的, 但細節實現錯誤導致半途而廢, 行百里者半九十!

還是上完整代碼, findChild前面已定義就不再貼了:

import 'package:flutter/foundation.dart'
import 'package:flutter/material.dart';
import 'utils/ElementUtils.dart';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Pages'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    Widget title = new Text(
      'another times: $_counter',
    );
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
            title,
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _counter++;
          Element e = findChild(context as Element, title);
          if (e != null) {
            title = new Text(
              'another times: $_counter',
            );
            e.owner.lockState(() {
              e.update(title);
            });
          }
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

現在只是重新創建了僅僅一個視圖哦, 它不快都不行~!

然而還是需要考慮一下這么做的缺點或者劣勢是什么
首先, 明顯的存在一個查詢操作, 這是由Element機制決定的, 遍歷只能通過訪問者模式, 時間復雜度O(n), 能不能避免這個查詢或者建立Widget到Element的映射? 也可以, 但是至少要查詢一次, 因為創建widget的時候Element可能還沒創建或者還沒有關聯, 只有Element樹建立完成之后才能查的到.
其次, 如果一個操作涉及多個視圖的更新, 我們不得不持有多個widget, 並查找多個widget對應的element, 還是有多個查詢操作, 這么麻煩還不如全部新建呢.

所以只能視情況而定, 沒有包打天下一勞永逸的方案, 合適的才是最好的!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM