Flutter Widget框架之旅

  • 介紹
  • 你好,世界
  • 基本的小部件
  • 使用材料組件
  • 處理手勢
  • 根據輸入更改小部件
  • 把它們放在一塊兒
  • 響應小部件生命週期事件
  • key
  • 全局Key

介紹

Flutter小部件採用現代反應式框架構建,從React中得到靈感。 中心思想是你從小部件中構建你的UI。 小組件描述了他們的視圖在給定其當前配置和狀態時應該看起來像什麼。 當小部件的狀態發生變化時,小部件會從新構建它的描述,該描述與前面的描述不一樣,以肯定底層渲染樹從一個狀態轉換到下一個狀態所需的最小更改。html

注意:若是您想經過深刻了解某些代碼來熟悉Flutter,請查看構建Flutter佈局併爲Flutter App添加交互功能java

你好,世界

最小的Flutter應用程序只需使用一個小部件調用runApp函數:react

import 'package:flutter/material.dart';

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

runApp函數使用給定的Widget並使其成爲Widget樹的根。 在此示例中,部件樹由兩個小部件組成,即Center部件及其子部件,即Text部件。框架強制根部件覆蓋屏幕,這意味着文本「Hello, world」最終集中在屏幕上。文本方向須要在此實例中指定; 當使用MaterialApp部件時,將爲您處理好,稍後將進行演示。git

在編寫應用程序時,一般會根據您的部件是否管理任何狀態來建立新的部件,這些部件是StatelessWidgetStatefulWidget的子類。部件的主要工做是實現一個build函數,它根據其餘較低級別的部件描述部件。該框架將依次構建這些部件,直到該過程落在表明底層RenderObject的部件中,該部件計算並描述部件的幾何形狀。github

基本的部件

主要文章:部件集概述 - 佈局模型json

Flutter帶有一套強大的基本小部件,其中如下是很是經常使用的:架構

  • TextText小部件可以讓您在應用程序中建立一段樣式文本。
  • Row,Column:這些柔性小部件可以讓您在水平(Row)和垂直(Column)方向上建立靈活的佈局。 其設計基於Web的Flexbox佈局模型。
  • StackStack小部件不是以線性方式(水平或垂直方向)進行堆疊,而是使用堆疊順序將小部件堆疊在彼此之上。而後,您能夠在堆棧的子項上使用Positioned小部件,以相對於堆棧的頂部,右側,底部或左側邊緣定位它們。Stacks基於Web的絕對定位佈局模型。
  • ContainerContainer小部件可以讓您建立矩形視覺元素。 一個容器能夠裝飾一個BoxDecoration,好比背景,邊框或陰影。Container也能夠有邊距,填充和約束應用於其大小。 另外,Container可使用矩陣在三維空間中轉換。

如下是一些簡單的小部件,它們結合了這些和其餘小部件:app

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條目。 它容許使用預約義的一組材料圖標框架

name: my_app
flutter:
  uses-material-design: true

爲了繼承主題數據,許多小部件須要位於MaterialApp中才能正常顯示。 所以,咱們使用MaterialApp運行應用程序。less

MyAppBar小部件建立一個Container,其高度爲56個設備無關像素,內部填充像素爲8像素,均位於左側和右側。在容器內部,MyAppBar使用Row佈局來組織其子項。中間的孩子,標題小部件被標記爲Expanded,這意味着它擴展以填充其餘孩子還沒有消費的剩餘可用空間。您能夠有多個Expanded子項,並使用Expandedflex參數肯定它們佔用可用空間的比率。

MyScaffold小部件在垂直列中組織其子女。在列頂部,它放置了MyAppBar的一個實例,將應用程序欄傳遞給一個Text小部件用做其標題。將小部件做爲參數傳遞給其餘小部件是一種強大的技術,可讓您建立能夠以各類方式重用的通用小部件。最後,MyScaffold使用Expanded來填充剩餘空間,其中包含一箇中心消息。

使用材料組件

主要文章:小工具概述 - 材料組件

Flutter提供了許多小工具,可幫助您構建遵循Material Design的應用程序。材質應用程序以MaterialApp小部件開始,該小部件在應用程序根部建立了許多有用的小部件,其中包括一個Navigator,該導航器管理由字符串(也稱爲「routes」)標識的小部件堆棧。Navigator可以讓您在應用程序的各個屏幕之間平滑過渡。 使用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,
      ),
    );
  }
}

如今咱們已經從MyAppBarMyScaffold切換到material.dartAppBarScaffold窗口小部件,咱們的應用程序開始查看更多的Material。例如,應用欄有一個陰影,標題文本會自動繼承正確的樣式。 咱們還添加了一個浮動動做按鈕,以便您採起措施。

請注意,咱們再次將小部件做爲參數傳遞給其餘小部件。Scaffold小部件將許多不一樣的小部件做爲命名參數,每一個小部件放置在適當位置的Scaffold佈局中。一樣,AppBar小部件容許咱們傳遞小部件以獲取title小部件的leadingactiions。這種模式在整個框架中重複出現,而且在設計本身的小部件時可能會考慮到這一點。

處理手勢

主要文章:Flutter的手勢

大多數應用程序包括某種形式的與系統的用戶交互。 構建交互式應用程序的第一步是檢測輸入手勢。 讓咱們經過建立一個簡單的按鈕來了解它的工做原理:

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小部件沒有可視化表示,而是檢測用戶作出的手勢。當用戶點擊Container時,GestureDetector將調用其onTap回調,在這種狀況下,將消息打印到控制檯。您可使用GestureDetector檢測各類輸入手勢,包括點擊,拖動和縮放。

許多小部件使用GestureDetector爲其餘小部件提供可選的回調。 例如,IconButtonRaisedButtonFloatingActionButton小部件具備onPressed回調,這些回調在用戶輕擊小部件時觸發。

根據輸入更改小部件

主要文章:StatefulWidgetState.setState

到目前爲止,咱們只使用無狀態的小部件。 無狀態小部件從他們的父部件接收參數,它們存儲在final的成員變量中。 當一個小部件被要求build時,它會使用這些存儲的值來爲它建立的小部件派生新的參數。

爲了構建更復雜的體驗 - 例如,以更有趣的方式對用戶輸入作出反應 - 應用程序一般會攜帶一些狀態。Flutter使用StatefulWidgets來捕捉這個想法。 StatefulWidgets是特殊的小部件,它知道如何生成狀態對象,而後用它來保持狀態。考慮這個基本的例子,使用前面提到的RaisedButton

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'),
      ],
    );
  }
}

您可能想知道爲何StatefulWidget和State是單獨的對象。 在Flutter中,這兩種類型的對象具備不一樣的生命週期。 小部件是臨時對象,用於構建當前狀態下的應用程序演示文稿。 另外一方面,State對象在調用build()之間是持久的,容許它們記住信息。

上面的例子接受用戶輸入並直接在其構建方法中使用結果。在更復雜的應用程序中,小部件層次結構的不一樣部分可能對不一樣的問題負責; 例如,一個小部件可能呈現一個複雜的用戶界面,其目標是收集特定信息(如日期或位置),而另外一個小部件可能會使用該信息來更改總體呈現。

在Flutter中,更改通知經過回調的方式「向上」流,而當前狀態則「向下」流向呈現的無狀態小部件。重定向這一流程的共同父母是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),
    ]);
  }
}

注意咱們如何建立了兩個新的無狀態小部件,乾淨地分隔了顯示計數器(CounterDisplay)和更改計數器(CounterIncrementor)的顧慮。儘管最終結果與前一個示例相同,但責任分離容許將更大的複雜性封裝在各個小部件中,同時保持父項的簡單性。

把它們放在一塊兒

讓咱們考慮一個更完整的例子,將上面介紹的概念聚集在一塊兒。 咱們將與一個假設的購物應用程序一塊兒工做,該應用程序顯示出售的各類產品,並維護用於預期購買的購物車。 咱們首先定義咱們的演示類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小部件遵循無狀態小部件的常見模式。 它將它在構造函數中接收到的值存儲在final的成員變量中,而後在build函數中使用它。例如,inCart布爾值能夠在兩個可視外觀之間切換:一個使用當前主題的主要顏色,另外一個使用灰色。

當用戶點擊列表項時,小部件不會直接修改其inCart值。 相反,小部件會調用它從其父部件接收到的onCartChanged函數。此模式可以讓您在小部件層次結構中存儲更高層級的狀態,從而使狀態持續更長的時間。 在極端狀況下,傳遞給runApp的存儲在窗口小部件上的狀態會在應用程序的整個生命週期中持續存在。

當父級收到onCartChanged回調時,父級將更新其內部狀態,這將觸發父級重建並使用新的inCart值建立ShoppingListItem的新實例。儘管父級在重建時建立了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小部件首次插入到樹中時,框架將調用createState函數來建立_ShoppingListState的新實例,以便與該樹中的該位置關聯。(請注意,咱們一般使用前導下劃線來命名State的子類,以指示它們是私有實現細節。)當此小部件的父級重建時,父級將建立ShoppingList的新實例,但該框架將從新使用樹已存在的_ShoppingListState實例 而不是再次調用createState

要訪問當前ShoppingList的屬性,_ShoppingListState可使用其widget屬性。若是父級重建並建立新的ShoppingList,則_ShoppingListState也將使用新的widget值重建。若是您但願在小部件屬性發生更改時收到通知,您能夠覆蓋didWargetWidget函數,該函數經過oldWidget傳遞,以便將舊小部件與當前widget進行比較。

在處理onCartChanged回調時,_ShoppingListState會經過添加或刪除_shoppingCart中的產品來改變其內部狀態。爲了通知框架它改變了它的內部狀態,它將這些調用包裝在setState調用中。調用setState會將這個小部件標記爲骯髒,並計劃在下一次您的應用程序須要更新屏幕時從新構建它。若是您在修改窗口小部件的內部狀態時忘記調用setState,則框架將不知道您的窗口小部件是髒的,而且可能不會調用窗口小部件的build函數,這意味着用戶界面可能不會更新以反映已更改的狀態。

經過以這種方式管理狀態,您不須要編寫用於建立和更新子部件的單獨代碼。 相反,您只需實現能夠處理這兩種狀況的構建函數。

響應小部件生命週期事件

主要文章:State

在StatefulWidget上調用createState以後,框架將新的狀態對象插入樹中,而後在狀態對象上調用initStateState的一個子類能夠覆蓋initState來完成只須要發生一次的工做。 例如,您能夠覆蓋initState來配置動畫或訂閱平臺服務。 initState的實現須要經過調用super.initState來啓動。

當一個狀態對象再也不須要時,框架在狀態對象上調用dispose。 您能夠覆蓋dispose函數來執行清理工做。 例如,您能夠覆蓋dispose以取消定時器或取消訂閱平臺服務。 一般,經過調用super.dispose執行dispose

按鍵

主要文章:Key

您可使用鍵來控制框架在小部件重建時哪一個小部件匹配哪一個其餘小部件。默認狀況下,框架根據它們的runtimeType和它們出現的順序來匹配當前構建和之前構建中的小部件。使用鍵,框架要求兩個小部件具備相同的key以及相同的runtimeType

鍵在構建相同類型的部件的許多實例的部件中最有用。例如,ShoppingList窗口部件構建了足夠的ShoppingListItem實例來填充其可見區域:

  • 若是沒有鍵,當前構建中的第一個條目將始終與前一個構建中的第一個條目同步,即便在語義上,列表中的第一個條目剛剛滾動屏幕而且再也不在視口中可見。
  • 經過將列表中的每一個條目指定爲「semantic」鍵,無限列表能夠更有效,由於框架將同步條目與匹配的semantic鍵並所以具備類似(或相同)的可視外觀。此外,語義上同步條目意味着保留在有狀態子部件中的狀態將保持附加到相同的語義條目而不是在視口中的相同數字位置上的條目。

全局Key

主要文章:GlobalKey

您可使用全局鍵來惟一標識子窗口部件。 全局鍵在整個窗口部件層次結構中必須是全局惟一的,這與局部鍵不一樣,後者只須要在同級中惟一。 因爲它們是全局惟一的,所以可使用全局鍵來檢索與窗口部件關聯的狀態。

相關文章
相關標籤/搜索