Flutter- 組件框架之旅

image

以下內容基本翻譯自A Tour of the Flutter Widget Framework,翻譯的可能并不完全!作為自己學習的筆記,加入了自己的理解,可能有疏漏錯誤,歡迎指正!
PS:Widget可能會翻譯為小部件、組件、控件等等,都是一個東西,不要太在意細節

引言

Flutter組件使用現代的響應式框架(react-style framework)建立,靈感來源React。核心思想,通過組件(widget)構建UI。通過給組件(Widgets)設置它們當前的配置(configuration )和狀態(state)來描述它們(Widgets)的長相。當組件的狀態發生改變,組件重建它的描述,為了確定過度到下一個狀態所需最小改變,該描述是框架對比之前的描述等到的差異。

原文:When a widget’s state changes, the widget rebuilds its description, which the framework diffs against the previous description in order to determine the minimal changes needed in the underlying render tree to transition from one state to the next.

英語太次,翻譯不準確,個人理解大意:就是framework取得了前后兩個狀態的最小改變,沒有變的屬性不操作,改變了的算差值進行改變,而不是清空一個狀態,再設置另一個狀態。
tips:如果想通過深入代碼更好了解Flutter,查看Building Layouts in FlutterAdding Interactivity to Your Flutter App.

Hello World

最小的FlutterApp僅僅通過組件調用runApp函數。

import 'package:flutter/material.dart';

void main() {
  runApp(
    new Center(
      child: new Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
      ),
    ),
  );
}

runApp方法獲取到給它的組件( Widget)并把組件作為它的組件樹(widget tree)的根。
此例中,組件樹持有兩個組件, Center(繼承Align) 和它的子組件- Text 。框架強制根組件(The root of the widget tree)鋪滿(cover)屏幕,這意味著Hello Worldtext最終位于屏幕中心。此例中的text的方向需要指定。
The text direction needs to be specified in this instance; when the MaterialApp widget is used, this is taken care of for you, as demonstrated later.
當寫app時,你通常會創建 StatelessWidgetStatefulWidget的子類作為組件,繼承那個類取決與你的組件是否管理狀態state。一個組件的主要工作時實現build 方法,這個方法描述了這個組件與其他組件或子組件的條約(terms)。框架framework會依次創建這些組件,直到超出組件的底部,這代表計算和描述組件的幾何形狀的底層渲染對象RenderObject.
The framework will build those widgets in turn until the process bottoms out in widgets that represent the underlying RenderObject, which computes and describes the geometry of the widget.

Basic Widgets

Main article: Widgets Overview - Layout Models
Flutter自帶一套強大的基礎組件(Basic widgets),以下是其中一些常用的:

  • Text:Text用于創建帶樣式的文本
  • Row, Column: 這些靈活(flex)地組件可以讓你在水平(Row)和垂直(Column)方向創建靈活的布局。它的設計是基于Web的flexbox布局模型
  • Stack: Stack組件可以讓你的組件在繪制順序上層積(stack)在彼此頂部,而不是線性(水平或垂直)的。可以在子Stack上使用 Positioned組件來放置他們到這個Stack的top,right,bottom或left邊界。Stack設計是基于Web的absolute positioning layout model(絕對位置布局模型)。
  • Container: Container組件幫你創建一個矩形元素。一個Container可以被一個BoxDecoration修飾,如背景(background)、邊線(border)、陰影(shadow)。一個Container也可以有margin,padding和固定大小定義。另外,Container可以用矩陣(matrix)在三維控件改變。
    以下是一些組合這些組件的例子:
import 'package:flutter/material.dart';

class MyAppBar extends StatelessWidget {
  MyAppBar({this.title});

  // Fields in a Widget subclass are always marked "final".

  final Widget title;

  @override
  Widget build(BuildContext context) {
    return new Container(
      height: 56.0, // in logical pixels
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      decoration: new BoxDecoration(color: Colors.blue[500]),
      // Row is a horizontal, linear layout.
      child: new Row(
        // <Widget> is the type of items in the list.
        children: <Widget>[
          new IconButton(
            icon: new Icon(Icons.menu),
            tooltip: 'Navigation menu',
            onPressed: null, // null disables the button
          ),
          // Expanded expands its child to fill the available space.
          new Expanded(
            child: title,
          ),
          new IconButton(
            icon: new Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
    );
  }
}

class MyScaffold extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Material is a conceptual piece of paper on which the UI appears.
    return new Material(
      // Column is a vertical, linear layout.
      child: new Column(
        children: <Widget>[
          new MyAppBar(
            title: new Text(
              'Example title',
              style: Theme.of(context).primaryTextTheme.title,
            ),
          ),
          new Expanded(
            child: new Center(
              child: new Text('Hello, world!'),
            ),
          ),
        ],
      ),
    );
  }
}

void main() {
  runApp(new MaterialApp(
    title: 'My app', // used by the OS task switcher
    home: new MyScaffold(),
  ));
}

確保在pubspec.yaml文件的 flutter 一節下有 uses-material-design: true 設置,它允許使用預定義的Material icons集合。

name: my_app
flutter:
  uses-material-design: true
運行效果

許多組件需要在 MaterialApp內部才正確顯示,繼承他們的主題數據(Theme data),因此我們運行一個MaterialApp程序。
MyAppBar組件創建一個高56dip( device-independent pixels)及內部padding 8px,從左到右的Container組件。在Container中,MyAppBar使用Row布局(layout)來管理它的子控件。中間兒子,title組件,標記為Expanded,意為它可以擴展填充任何剩余的、未被其他子控件占用的空間。你可能有多重Expanded子控件,使用flex來確定他們各自占用可用空間的比例(You can have multiple Expanded children and determine the ratio in which they consume the available space using the flex argument to Expanded)。
MyScaffold組件在垂直列方向(vertical column)管理它的子控件。在列頂,它放了一個MyAppBar實例,傳遞一個Text組件做app bar的title。傳遞組件(Passing widgets)作為另一個組建的參數是一個強大的技術,它允許你創建的常用組件多樣重用。最后,居中顯示信心的MyScaffold使用Expanded來填充剩余的空間。

Using Material Components

Main article: Widgets Overview - Material Components
Flutter提供了許多遵循Material Design的組件幫助你創建app。一個Material app始于MaterialApp組件,MaterialApp組件作為你app的根(root)創建許多有用的組件,包括 管理一堆使用strings區分的組件、亦被稱為routesNavigatorNavigator 讓你平滑在app的screens間切換。使用MaterialApp不是必須的,但是是一個很好的慣例。

import 'package:flutter/material.dart';

void main() {
  runApp(new MaterialApp(
    title: 'Flutter Tutorial',
    home: new TutorialHome(),
  ));
}

class TutorialHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Scaffold is a layout for the major Material Components.
    return new Scaffold(
      appBar: new AppBar(
        leading: new IconButton(
          icon: new Icon(Icons.menu),
          tooltip: 'Navigation menu',
          onPressed: null,
        ),
        title: new Text('Example title'),
        actions: <Widget>[
          new IconButton(
            icon: new Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
      // body is the majority of the screen.
      body: new Center(
        child: new Text('Hello, world!'),
      ),
      floatingActionButton: new FloatingActionButton(
        tooltip: 'Add', // used by assistive technologies
        child: new Icon(Icons.add),
        onPressed: null,
      ),
    );
  }
}
運行效果

現在我們替換MyAppBar and MyScaffold為來自material.dartAppBarScaffold ,我們的app看起來更加Material。例如,app bar有陰影了,title文本自動繼承了正確的樣式。我們也添加了一個合適的浮動按鈕(FloatingActionButton)。

注意,我們再次把組件作為參數傳遞給另一個組件。Scaffold需要許多不同的組件作為參數,他們將被放在Scaffold的適當位置。類似的,AppBar組件讓我們傳遞組件作為 title組件的leadingactions 。這種模式遍布整個框架,你在設計自己的組件時也需要考慮。

Handling gestures 處理手勢

Main article: Gestures in Flutter
大部分app包含一些用戶與系統交互的表單。第一步就是要創建一個可交互的app去檢測輸入的手勢getsures。創建一個實例按鈕來看看這是如何工作的:

class MyButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new GestureDetector(
      onTap: () {
        print('MyButton was tapped!');
      },
      child: new Container(
        height: 36.0,
        padding: const EdgeInsets.all(8.0),
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        decoration: new BoxDecoration(
          borderRadius: new BorderRadius.circular(5.0),
          color: Colors.lightGreen[500],
        ),
        child: new Center(
          child: new Text('Engage'),
        ),
      ),
    );
  }
}

GestureDetector 組件不可見,但可以檢測用戶手勢gestures。當用戶輕擊ContainerGestureDetector將回調它的onTap方法,這個例子中,在控制臺console打印了一條消息。你可以使用GestureDetector來檢測一系列輸入手勢,包括點擊tap,拖動drag和縮放scale。
許多組件使用GestureDetector來用于其他組件的可選回調。例如,IconButton,RaisedButton, FloatingActionButton 有一個 onPressed 回調方法,當用戶點擊這些組件時觸發。

Changing widgets in response to input

Main articles: StatefulWidget, State.setState
目前,我們僅使用了stateless組件。Stateless widgets從他們的父類接受參數,他們保存了 final 成員變量。當一個組件調用build時,它使用這些已存的值提取新參數(derive new arguments )來創建組件。
為了創建更加復雜的體驗,例如,以更有趣的方式響應用戶的輸入,app通常帶有某些狀態。Flutter使用StatefulWidgets組件獲取這些idea.StatefulWidgets是知道如何創建State對象的特殊組件,State持有state。在此例中,使用RaisedButton mentioned earlier:

class Counter extends StatefulWidget {
  // This class is the configuration for the state. It holds the
  // values (in this nothing) provided by the parent and used by the build
  // method of the State. Fields in a Widget subclass are always marked "final".

  @override
  _CounterState createState() => new _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      // This call to setState tells the Flutter framework that
      // something has changed in this State, which causes it to rerun
      // the build method below so that the display can reflect the
      // updated values. If we changed _counter without calling
      // setState(), then the build method would not be called again,
      // and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance
    // as done by the _increment method above.
    // The Flutter framework has been optimized to make rerunning
    // build methods fast, so that you can just rebuild anything that
    // needs updating rather than having to individually change
    // instances of widgets.
    return new Row(
      children: <Widget>[
        new RaisedButton(
          onPressed: _increment,
          child: new Text('Increment'),
        ),
        new Text('Count: $_counter'),
      ],
    );
  }
}
運行效果

你可能好奇,為什么StatefulWidgetState是分離的對象。在Flutter中,這兩類對象有不同的生命周期。Widgets是臨時對象,用于構建app當前狀態的表達(presentation)。另一方面,State是在build()之間是持續的,允許他們記憶信息。
上例中,接收用戶輸入和也接受它的build方法中的結果。在更復雜的app中,不同層級的組件可能負責不同關注點。例如,一個組件可能呈現復雜的用戶界面,其目的是收集特定信息,如信息或位置,而另一個組件可能使用這些信息更改總體表現。
在Flutter中,更改信息流依賴回調組件層次結構,當當前狀態流向stateless組件。State就是父類重定向這些信息流。我們來看看實際中是如何工作的,這是一個稍微復雜的例子:

class CounterDisplay extends StatelessWidget {
  CounterDisplay({this.count});

  final int count;

  @override
  Widget build(BuildContext context) {
    return new Text('Count: $count');
  }
}

class CounterIncrementor extends StatelessWidget {
  CounterIncrementor({this.onPressed});

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return new RaisedButton(
      onPressed: onPressed,
      child: new Text('Increment'),
    );
  }
}

class Counter extends StatefulWidget {
  @override
  _CounterState createState() => new _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      ++_counter;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Row(children: <Widget>[
      new CounterIncrementor(onPressed: _increment),
      new CounterDisplay(count: _counter),
    ]);
  }
}

請注意我們如何創建2個stateless組件,清晰分離了關注點:顯示displaying計數器(CounterDisplay)和改變changing計數器(CounterIncrementor)。雖然最終結果和之前一樣,但費力責任允許在單個組件中加入更多復雜性,同時保持父級的簡單性。

Bringing it all together

我們來思考一個更復雜的例子,把以上觀點都匯聚在一起。我們來假設一個購物app,展示各種待售產品,并維護一個購物車用于購買。我們先來定義 presentation class-ShoppingListItem:

class Product {
  const Product({this.name});
  final String name;
}

typedef void CartChangedCallback(Product product, bool inCart);

class ShoppingListItem extends StatelessWidget {
  ShoppingListItem({Product product, this.inCart, this.onCartChanged})
      : product = product,
        super(key: new ObjectKey(product));

  final Product product;
  final bool inCart;
  final CartChangedCallback onCartChanged;

  Color _getColor(BuildContext context) {
    // The theme depends on the BuildContext because different parts of the tree
    // can have different themes.  The BuildContext indicates where the build is
    // taking place and therefore which theme to use.

    return inCart ? Colors.black54 : Theme.of(context).primaryColor;
  }

  TextStyle _getTextStyle(BuildContext context) {
    if (!inCart) return null;

    return new TextStyle(
      color: Colors.black54,
      decoration: TextDecoration.lineThrough,
    );
  }

  @override
  Widget build(BuildContext context) {
    return new ListTile(
      onTap: () {
        onCartChanged(product, !inCart);
      },
      leading: new CircleAvatar(
        backgroundColor: _getColor(context),
        child: new Text(product.name[0]),
      ),
      title: new Text(product.name, style: _getTextStyle(context)),
    );
  }
}

ShoppingListItem繼承自一個stateless 組件的通用模式。它保存來自它的構造函數接受的值給它的 final 成員變量,這些值在它的build方法中使用。例如,inCart切換兩種可視外觀,一個使用當前主題的primary顏色,另一個使用灰色gray。
當用戶點擊條目,組件不直接改變它的inCart的值。而是調用父類的onCartChanged方法。這種模式讓你保存狀態state到更高的組件層級,這是狀態持續更長時期的原因。這個例子中,保存在組件中的狀態state通過runApp持續存在于app的生命周期中。
當父類收到onCartChanged回調,父類將更新它的內部狀態state,這將引發父類重建并新建一個帶有新inCart值的新的ShopingListItem實例。雖然父類重建時創建一個新的ShoppingListItem實例,但是這個操作是廉價的,因為框架對比了新的組件和舊的組件,只應用不同的RenderObject
來看一個保存可變狀態的父類:

class ShoppingList extends StatefulWidget {
  ShoppingList({Key key, this.products}) : super(key: key);

  final List<Product> products;

  // The framework calls createState the first time a widget appears at a given
  // location in the tree. If the parent rebuilds and uses the same type of
  // widget (with the same key), the framework will re-use the State object
  // instead of creating a new State object.

  @override
  _ShoppingListState createState() => new _ShoppingListState();
}

class _ShoppingListState extends State<ShoppingList> {
  Set<Product> _shoppingCart = new Set<Product>();

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      // When user changes what is in the cart, we need to change _shoppingCart
      // inside a setState call to trigger a rebuild. The framework then calls
      // build, below, which updates the visual appearance of the app.

      if (inCart)
        _shoppingCart.add(product);
      else
        _shoppingCart.remove(product);
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Shopping List'),
      ),
      body: new ListView(
        padding: new EdgeInsets.symmetric(vertical: 8.0),
        children: widget.products.map((Product product) {
          return new ShoppingListItem(
            product: product,
            inCart: _shoppingCart.contains(product),
            onCartChanged: _handleCartChanged,
          );
        }).toList(),
      ),
    );
  }
}

void main() {
  runApp(new MaterialApp(
    title: 'Shopping App',
    home: new ShoppingList(
      products: <Product>[
        new Product(name: 'Eggs'),
        new Product(name: 'Flour'),
        new Product(name: 'Chocolate chips'),
      ],
    ),
  ));
}

運行效果

ShoppingList類繼承自 保存可變狀態的 StatefulWidget。當ShoppingList組件首次插入樹(tree)中,框架調用createState方法來創建一個最新的_ShoppingListState實例來連接樹(tree)。(注意我們通常使用_開頭來定義State的子類來表示他們私有實現詳情(private implementation details)) 當組件的父類重建,父類會創建一個新的ShoppingList實例,但是框架會重用已經存在樹(tree)中的_ShoppingListState實例,而不是再次調用createState
為了獲取當前ShoppingList特性(properties ),_ShoppingListState會使用它自身widget特性。如果父類重建并創建一個新的ShoppingList,_ShoppingListState也會重新創建一個新的widget值。如果你希望當 widget 特性改變時得到通知,可以復寫didUpdateWidget方法,它會傳遞oldWidget,你可以與當前widget進行對比。
當處理onCartChanged回調時,_ShoppingListState通過給_shoppingCart增加或刪除一個product改變它內部狀態。通過調用setState 通知框架(framework)它內部狀態(state)的改變。調用setState標記widget為臟dirty并安排它在下次更新屏幕時重建。如果內部狀態改變時你忘記調用setState,框架(Framework)不會知道你的widget是臟的(dirty),也就不會調用widgetbuild方法,這意味著,用戶界面不會更新反應狀態的改變。
通過這種方式管理state,你不必分別為創建和更新子widget寫代碼,只需簡單的實現build方法,它會處理兩種情況。

Responding to widget lifecycle events 組件生命周期響應

Main article: State
StatefulWidget調用 createState之后,框架(framework)插入一個新的state對象到樹(tree)中,然后調用state對象的 initStateState的子類可以復寫(override)initState來做只需執行一次的工作。例如,你可以復寫(override)initState來配置動畫或訂閱平臺服務(subscribe to platform services)。initState的實現要求以調用super.initState開始。
當一個state對象不在被需要,框架(framework)調用state對象的dispose 。你可以復寫(override)dispose方法來清理工作。例如,通過復寫dispose來取消計時器或解除平臺服務訂閱。dispose實現通常以調用super.dispose結束。

Keys

Main article: Key
你可以使用keys來控制當一個組件重建時,框架將匹配那些其他組件。默認的,框架會根據他們的 runtimeType和他們出現的順序匹配當前和前一個組件。帶有keys的,框架(framework)會要求2個組件有相同的key和相同的runtimeType
當創建很多相同類型組件的實例時Keys是非常有用的。例如,上例中ShoppingList組件,創建了足夠多的ShoppingListItem實例來填充它的可視區域:

  • 沒有keys,當前創建的第一個entry會一直與前一次創建的第一個entry同步,即使列表中的第一個entry滾出屏幕而且不再可見。
    通過指派列表中每一個entry一個semantic* key,無限列表可以更有效,因為框架會同步entries 匹配semantic keys和因此相同或近似的外觀。此外,同步entries語義semantically意味著在stateful的子組件中的state會保持連接到相同semantic entry,而不是連接到在viewport中的相同數值位置的entry。

Global Keys 全局Keys

Main article: GlobalKey
你可以使用全局keys唯一的標識子組件。全局keys必須在整個組件層級是全局唯一的,不像本地local key只需在兄弟之間唯一。因為他們全局唯一,可以用全局key檢索與組件連接狀態。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容