flutter防止widget rebuild終極解決辦法

背景

衆所周知,flutter是借鑑了前端框架React的思想而開發的框架,有不少類似之處,也有看不到的不同,我目前感覺最深的就是flutter無所不在的rebuild,那麼有辦法阻止rebuild嗎?有!html

在widget前面加const

這個辦法確實能夠,一勞永逸,可是你一旦加了const,你這個widget就永遠不會更新了,除非你是在寫靜態頁面,不然你最好不要用它前端

把你的組件寫成 「葉子"組件

參考flutter文檔 就是把那你的組件都定義成葉子,樹的最底層,而後你在葉子組件內部更改狀態,這樣葉子之間互不影響,emm,在我看來這樣子跟react的狀態提高的思想相反了,由於你爲了互不影響,你不能把狀態放到根節點,放到根節點,一調用setState那所有自組價就rebuild了,我一開始一直是用這個思路來解決rebuild的問題的, 好比使用StreamBuilder這個能夠包裹你的組件,而後用流來觸發StreamBuilder內部rebuild,經過StreamBuilder來隔絕外面的組件,這樣寫有個小缺點,我要額外寫個流,還要關閉流,很囉嗦。react

使用其餘的庫,好比Provider

你能夠看到Provider庫的做者提供了一些Widget來減小rebuild,可是我感受都不太簡潔,易用 這些庫的實現方法跟StreamBuilder差很少,都是經過一個Widget來隔絕其餘Widget,讓更新限制在內部,可是都有一個共同點,你要配合額外的外部變量去觸發內部的更新git

終極辦法

用過react的人都知道,react的類組件有個很重要的生命週期叫shouldComponentUpdate,咱們能夠在組件內部重寫這個聲明週期來進行性能優化。github

如何優化呢,就是對比組件的新舊props的屬性的值是否一致,若是一致那組件就不必更新. 那flutter有沒有相似的生命週期呢?沒有!算法

flutter團隊認爲flutter的渲染速度已經夠快了,而且flutter實際也有相似react 的diff算法來對比element是否須要更新,他們作了優化和緩存,由於更新flutter的element是很昂貴的操做,而rebuild Widget只是從新new 了一個widget的實例,就像只是執行了一段dart代碼同樣,沒涉及到任何ui層的更改,並且他們也對新舊widget作了diff,經過diff widget來減小對element層的更改,無論怎樣,只要沒有致使element銷燬,重建,通常不會影響什麼性能。api

可是經過谷歌和百度你仍是能發現有人在搜索如何防止rebuild,這說明了市場仍是有需求的。我我的認爲,這個不叫過分優化,實際上是有這個場景須要優化的,好比谷歌推薦的狀態管理庫Provider就提供瞭如何減小沒必要要的rebuild的方法緩存

話(我)不(想)多(吐)說(槽)了:性能優化

library should_rebuild_widget;

import 'package:flutter/material.dart';

typedef ShouldRebuildFunction<T> = bool Function(T oldWidget, T newWidget);

class ShouldRebuild<T extends Widget> extends StatefulWidget {
  final T child;
  final ShouldRebuildFunction<T> shouldRebuild;
  ShouldRebuild({@required this.child, this.shouldRebuild}):assert((){
    if(child == null){
      throw FlutterError.fromParts(
          <DiagnosticsNode>[
            ErrorSummary('ShouldRebuild widget: builder must be not null')]
      );
    }
    return true;
  }());
  @override
  _ShouldRebuildState createState() => _ShouldRebuildState<T>();
}

class _ShouldRebuildState<T extends Widget> extends State<ShouldRebuild> {
  @override
  ShouldRebuild<T> get widget => super.widget;
  T oldWidget;
  @override
  Widget build(BuildContext context) {
    final T newWidget = widget.child;
    if (this.oldWidget == null || (widget.shouldRebuild == null ? true : widget.shouldRebuild(oldWidget, newWidget))) {
      this.oldWidget = newWidget;
    }
    return oldWidget;
  }
}

複製代碼

就是這幾行代碼,不到40行代碼 來看測試代碼:前端框架

import 'dart:math';

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Test(),
    );
  }
}

class Test extends StatefulWidget {
  @override
  _TestState createState() => _TestState();
}

class _TestState extends State<Test> {
  int productNum = 0;
  int counter = 0;

  _incrementCounter(){
    setState(() {
      ++counter;
    });
  }
  _incrementProduct(){
    setState(() {
      ++productNum;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Container(
          constraints: BoxConstraints.expand(),
          child: Column(
            children: <Widget>[
              ShouldRebuild<Counter>(
                shouldRebuild: (oldWidget, newWidget) => oldWidget.counter != newWidget.counter,
                child: Counter(counter: counter,onClick: _incrementCounter,title: '我是優化過的Counter',) ,
              ),
              Counter(
                counter: counter,onClick: _incrementCounter,title: '我是未優化過的Counter',
              ),
              Text('productNum = $productNum',style: TextStyle(fontSize: 22,color: Colors.deepOrange),),
              RaisedButton(
                onPressed: _incrementProduct,
                child: Text('increment Product'),
              )
            ],
          ),
        ),
      ),
    );
  }
}



class Counter extends StatelessWidget {
  final VoidCallback onClick;
  final int counter;
  final String title;
  Counter({this.counter,this.onClick,this.title});
  @override
  Widget build(BuildContext context) {
    Color color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
    return AnimatedContainer(
      duration: Duration(milliseconds: 500),
      color:color,
      height: 150,
      child:Column(
        children: <Widget>[
          Text(title,style: TextStyle(fontSize: 30),),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('counter = ${this.counter}',style: TextStyle(fontSize: 43,color: Colors.white),),
            ],
          ),
          RaisedButton(
            color: color,
            textColor: Colors.white,
            elevation: 20,
            onPressed: onClick,
            child: Text('increment Counter'),
          ),
        ],
      ),
    );
  }
}



複製代碼

佈局效果圖:

  • 咱們定義了一個Counter組件,Counter在build的過程當中會改變本身的背景色,每次執行build都會隨機生成背景色,以便咱們觀察組件是否build。另外Counter接收父組件傳過來的值counter,並展現,還接收一個title,來區分不一樣的Counter名字
  • 看這裏的代碼
Column(
            children: <Widget>[
              ShouldRebuild<Counter>(
                shouldRebuild: (oldWidget, newWidget) => oldWidget.counter != newWidget.counter,
                child:  Counter(counter: counter,onClick: _incrementCounter,title: '我是優化過的Counter',),
              ),
              Counter(
                counter: counter,onClick: _incrementCounter,title: '我是未優化過的Counter',
              ),
              Text('productNum = $productNum',style: TextStyle(fontSize: 22,color: Colors.deepOrange),),
              RaisedButton(
                onPressed: _incrementProduct,
                child: Text('increment Product'),
              )
            ],
          )
複製代碼

咱們上面的Counter被ShouldRebuild包裹,同時shouldRebuild參數傳入了自定義的條件當這個Counter接收的counter不一致時才rebuild,若是新老Counter對比發現counter一致那就不rebuild, 而下面的Counter則沒有作優化。

  • 咱們點擊增長Product的按鈕 increment Product,會觸發增長productNum,而此時沒有增長counter,因此被ShouldRebuild包裹的Counter並無rebuild,而下面沒有包裹的Counter就rebuild了 來看下gif:

原理揭祕

其實原理跟用const聲明的widget一致,來看下flutter源碼

Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
...
      if (child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        return child;
      }
      if (Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        assert(child.widget == newWidget);
        assert(() {
          child.owner._debugElementWasRebuilt(child);
          return true;
        }());
        return child;
      }

...
}
複製代碼

摘抄其中一部分, 第一個

if (child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        return child;
   }
複製代碼

這裏是關鍵,flutter發現child.widget也就是老的widget和新的widget是同一個,引用一致的話就直接返回了child

若是發現不一致就走了這裏

if (Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        assert(child.widget == newWidget);
        assert(() {
          child.owner._debugElementWasRebuilt(child);
          return true;
        }());
        return child;
      }
複製代碼

這裏若是能夠更新,就會走child.update(),這個方法一旦走了,那build方法確定會執行了。 請看它作了什麼事

@override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true;
    rebuild();
  }
複製代碼

看到rebuild()就知道必定去執行build了。

其實看到 if (child.widget == newWidget) 咱們也知道爲何 const Text()會讓Text不會重複build,由於常量是一直不會變的

總結

有了這個Widget咱們能夠把狀態都放在根組件,而後把頁面拆分紅多個子組件,而後用ShouldRebuild包裹子組件,同時設置rebuild的條件就能夠阻止 沒必要要的 重渲染。你能夠盡情的setState了,固然若是你的狀態被多個組件使用,這時候你就須要狀態管理了。 可是,可能有人會以爲是否過分優化,我我的以爲是否須要優化是根據你本身的狀況定的,若是某天用戶反饋你的頁面卡頓,那你就須要優化,又或者你以爲rebuild影響到了你的功能,好比動畫重複執行了,那你就須要阻止rebuild了。

github:shouldRebuild

若是以爲幫助到了你,請star一下吧

相關文章
相關標籤/搜索