做(zuò)自(zì)由與創造的先行(xíng)者

Flutter Widget框架概述

Flutter開(kāi)發手冊

介紹

Flutter Widget采用現代響應式框架構建,這(zhè)是從(cóng) React 中獲得的靈感,中心思想是用widget構建你的UI。 Widget描述了(le)他(tā)們的視(shì)圖在給定其當前配置和(hé)狀态時(shí)應該看(kàn)起來像什麽。當widget的狀态發生變化時(shí),widget會重新構建UI,Flutter會對比前後變化的不同, 以确定底層渲染樹(shù)從(cóng)一(yī)個狀态轉換到下(xià)一(yī)個狀态所需的最小更改(譯者語:類似于React/Vue中虛拟DOM的diff算法)。

注意: 如(rú)果您想通(tōng)過代碼來深入了(le)解Flutter,請(qǐng)查看(kàn) 構建Flutter布局 和(hé) 為(wèi)Flutter App添加交互功能(néng)。

Hello World

一(yī)個最簡單的Flutter應用程序,隻需一(yī)個widget即可(kě)!如(rú)下(xià)面示例:将一(yī)個widget傳給runApp函數(shù)即可(kě):

import 'package:flutter/material.dart';

void main() {

runApp(

new Center(

child: new Text(

'Hello, world!',

textDirection: TextDirection.ltr,

),

),

);

}

該runApp函數(shù)接受給定的Widget并使其成為(wèi)widget樹(shù)的根。 在此示例中,widget樹(shù)由兩個widget:Center(及其子widget)和(hé)Text組成。框架強制根widget覆蓋整個屏幕,這(zhè)意味着文本“Hello, world”會居中顯示在屏幕上(shàng)。文本顯示的方向需要(yào)在Text實例中指定,當使用MaterialApp時(shí),文本的方向将自(zì)動設定,稍後将進行(xíng)演示。

在編寫應用程序時(shí),通(tōng)常會創建新的widget,這(zhè)些widget是無狀态的StatelessWidget或者是有狀态的StatefulWidget, 具體的選擇取決于您的widget是否需要(yào)管理一(yī)些狀态。widget的主要(yào)工(gōng)作(zuò)是實現一(yī)個build函數(shù),用以構建自(zì)身。一(yī)個widget通(tōng)常由一(yī)些較低(dī)級别widget組成。Flutter框架将依次構建這(zhè)些widget,直到構建到最底層的子widget時(shí),這(zhè)些最低(dī)層的widget通(tōng)常為(wèi)RenderObject,它會計算并描述widget的幾何形狀。

基礎 Widget

主要(yào)文章(zhāng): widget概述-布局模型

Flutter有一(yī)套豐富、強大的基礎widget,其中以下(xià)是很(hěn)常用的:

Text:該 widget 可(kě)讓創建一(yī)個帶格式的文本。

Row、 Column: 這(zhè)些具有彈性空間(jiān)的布局類Widget可(kě)讓您在水平(Row)和(hé)垂直(Column)方向上(shàng)創建靈活的布局。其設計是基于web開(kāi)發中的Flexbox布局模型。

Stack: 取代線性布局 (譯者語:和(hé)Android中的LinearLayout相似),Stack允許子 widget 堆疊, 你可(kě)以使用 Positioned 來定位他(tā)們相對于Stack的上(shàng)下(xià)左右四條邊的位置。Stacks是基于Web開(kāi)發中的絕度定位(absolute positioning )布局模型設計的。

Container: Container 可(kě)讓您創建矩形視(shì)覺元素。container 可(kě)以裝飾為(wèi)一(yī)個BoxDecoration, 如(rú) background、一(yī)個邊框、或者一(yī)個陰影。 Container 也可(kě)以具有邊距(margins)、填充(padding)和(hé)應用于其大小的約束(constraints)。另外(wài), Container可(kě)以使用矩陣在三維空間(jiān)中對其進行(xíng)變換。

以下(xià)是一(yī)些簡單的Widget,它們可(kě)以組合出其它的Widget:

import 'package:flutter/material.dart';

class MyAppBar extends StatelessWidget {

MyAppBar({this.title});

// Widget子類中的字段往往都(dōu)會定義為(wèi)"final"

final Widget title;

@override

Widget build(BuildContext context) {

return new Container(

height: 56.0, // 單位是邏輯上(shàng)的像素(并非真實的像素,類似于浏覽器中的像素)

padding: const EdgeInsets.symmetric(horizontal: 8.0),

decoration: new BoxDecoration(color: Colors.blue[500]),

// Row 是水平方向的線性布局(linear layout)

child: new Row(

//列表項的類型是

children: [

new IconButton(

icon: new Icon(Icons.menu),

tooltip: 'Navigation menu',

onPressed: null, // null 會禁用 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 是UI呈現的“一(yī)張紙(zhǐ)”

return new Material(

// Column is 垂直方向的線性布局.

child: new Column(

children: [

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(),

));

}

請(qǐng)确保在pubspec.yaml文件中,将flutter的值設置為(wèi):uses-material-design: true。這(zhè)允許我們可(kě)以使用一(yī)組預定義Material icons。

name: my_app

flutter:

uses-material-design: true

為(wèi)了(le)繼承主題數(shù)據,widget需要(yào)位于MaterialApp內(nèi)才能(néng)正常顯示, 因此我們使用MaterialApp來運行(xíng)該應用。

在MyAppBar中創建一(yī)個Container,高度為(wèi)56像素(像素單位獨立于設備,為(wèi)邏輯像素),其左側和(hé)右側均有8像素的填充。在容器內(nèi)部, MyAppBar使用Row 布局來排列其子項。 中間(jiān)的title widget被标記為(wèi)Expanded, ,這(zhè)意味着它會填充尚未被其他(tā)子項占用的的剩餘可(kě)用空間(jiān)。Expanded可(kě)以擁有多個children, 然後使用flex參數(shù)來确定他(tā)們占用剩餘空間(jiān)的比例。

MyScaffold 通(tōng)過一(yī)個Column widget,在垂直方向排列其子項。在Column的頂部,放置了(le)一(yī)個MyAppBar實例,将一(yī)個Text widget作(zuò)為(wèi)其标題傳遞給應用程序欄。将widget作(zuò)為(wèi)參數(shù)傳遞給其他(tā)widget是一(yī)種強大的技術(shù),可(kě)以讓您創建各種複雜(zá)的widget。最後,MyScaffold使用了(le)一(yī)個Expanded來填充剩餘的空間(jiān),正中間(jiān)包含一(yī)條message。

使用 Material 組件

主要(yào)文章(zhāng): Widgets 總覽 - Material 組件

Flutter提供了(le)許多widgets,可(kě)幫助您構建遵循Material Design的應用程序。Material應用程序以MaterialApp widget開(kāi)始, 該widget在應用程序的根部創建了(le)一(yī)些有用的widget,其中包括一(yī)個Navigator, 它管理由字符串标識的Widget棧(即頁面路由棧)。Navigator可(kě)以讓您的應用程序在頁面之間(jiān)的平滑的過渡。 是否使用MaterialApp完全是可(kě)選的,但(dàn)是使用它是一(yī)個很(hěn)好的做(zuò)法。

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是Material中主要(yào)的布局組件.

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: [

new IconButton(

icon: new Icon(Icons.search),

tooltip: 'Search',

onPressed: null,

),

],

),

//body占屏幕的大部分

body: new Center(

child: new Text('Hello, world!'),

),

floatingActionButton: new FloatingActionButton(

tooltip: 'Add', // used by assistive technologies

child: new Icon(Icons.add),

onPressed: null,

),

);

}

}

現在我們已經從(cóng)MyAppBar和(hé)MyScaffold切換到了(le)AppBar和(hé) Scaffold widget, 我們的應用程序現在看(kàn)起來已經有一(yī)些“Material”了(le)!例如(rú),應用欄有一(yī)個陰影,标題文本會自(zì)動繼承正确的樣式。我們還添加了(le)一(yī)個浮動操作(zuò)按鈕,以便進行(xíng)相應的操作(zuò)處理。

請(qǐng)注意,我們再次将widget作(zuò)為(wèi)參數(shù)傳遞給其他(tā)widget。該 Scaffold widget 需要(yào)許多不同的widget的作(zuò)為(wèi)命名參數(shù),其中的每一(yī)個被放置在Scaffold布局中相應的位置。 同樣,AppBar 中,我們給參數(shù)leading、actions、title分别傳一(yī)個widget。 這(zhè)種模式在整個框架中會經常出現,這(zhè)也可(kě)能(néng)是您在設計自(zì)己的widget時(shí)會考慮到一(yī)點。

處理手勢

主要(yào)文章(zhāng): Flutter中的手勢

大多數(shù)應用程序包括某種形式與系統的交互。構建交互式應用程序的第一(yī)步是檢測輸入手勢。讓我們通(tōng)過創建一(yī)個簡單的按鈕來了(le)解它的工(gōng)作(zuò)原理:

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 widget并不具有顯示效果,而是檢測由用戶做(zuò)出的手勢。 當用戶點擊Container時(shí), GestureDetector會調用它的onTap回調, 在回調中,将消息打印到控制台。您可(kě)以使用GestureDetector來檢測各種輸入手勢,包括點擊、拖動和(hé)縮放。

許多widget都(dōu)會使用一(yī)個GestureDetector為(wèi)其他(tā)widget提供可(kě)選的回調。 例如(rú),IconButton、 RaisedButton、 和(hé)FloatingActionButton ,它們都(dōu)有一(yī)個onPressed回調,它會在用戶點擊該widget時(shí)被觸發。

根據用戶輸入改變widget

主要(yào)文章(zhāng): StatefulWidget, State.setState

到目前為(wèi)止,我們隻使用了(le)無狀态的widget。無狀态widget從(cóng)它們的父widget接收參數(shù), 它們被存儲在final型的成員(yuán)變量中。 當一(yī)個widget被要(yào)求構建時(shí),它使用這(zhè)些存儲的值作(zuò)為(wèi)參數(shù)來構建widget。

為(wèi)了(le)構建更複雜(zá)的體驗 - 例如(rú),以更有趣的方式對用戶輸入做(zuò)出反應 - 應用程序通(tōng)常會攜帶一(yī)些狀态。 Flutter使用StatefulWidgets來滿足這(zhè)種需求。StatefulWidgets是特殊的widget,它知道(dào)如(rú)何生成State對象,然後用它來保持狀态。 思考下(xià)面這(zhè)個簡單的例子,其中使用了(le)前面提到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 {

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: [

new RaisedButton(

onPressed: _increment,

child: new Text('Increment'),

),

new Text('Count: $_counter'),

],

);

}

}

您可(kě)能(néng)想知道(dào)為(wèi)什麽StatefulWidget和(hé)State是單獨的對象。在Flutter中,這(zhè)兩種類型的對象具有不同的生命周期: Widget是臨時(shí)對象,用于構建當前狀态下(xià)的應用程序,而State對象在多次調用build()之間(jiān)保持不變,允許它們記住信息(狀态)。

上(shàng)面的例子接受用戶點擊,并在點擊時(shí)使_counter自(zì)增,然後直接在其build方法中使用_counter值。在更複雜(zá)的應用程序中,widget結構層次的不同部分可(kě)能(néng)有不同的職責; 例如(rú),一(yī)個widget可(kě)能(néng)呈現一(yī)個複雜(zá)的用戶界面,其目标是收集特定信息(如(rú)日期或位置),而另一(yī)個widget可(kě)能(néng)會使用該信息來更改整體的顯示。

在Flutter中,事件流是“向上(shàng)”傳遞的,而狀态流是“向下(xià)”傳遞的(譯者語:這(zhè)類似于React/Vue中父子組件通(tōng)信的方式:子widget到父widget是通(tōng)過事件通(tōng)信,而父到子是通(tōng)過狀态),重定向這(zhè)一(yī)流程的共同父元素是State。讓我們看(kàn)看(kàn)這(zhè)個稍微(wēi)複雜(zá)的例子是如(rú)何工(gōng)作(zuò)的:

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 {

int _counter = 0;

void _increment() {

setState(() {

++_counter;

});

}

@override

Widget build(BuildContext context) {

return new Row(children: [

new CounterIncrementor(onPressed: _increment),

new CounterDisplay(count: _counter),

]);

}

}

注意我們是如(rú)何創建了(le)兩個新的無狀态widget的!我們清晰地(dì)分離了(le) 顯示 計數(shù)器(CounterDisplay)和(hé) 更改 計數(shù)器(CounterIncrementor)的邏輯。 盡管最終效果與前一(yī)個示例相同,但(dàn)責任分離允許将複雜(zá)性邏輯封裝在各個widget中,同時(shí)保持父項的簡單性。

整合所有

讓我們考慮一(yī)個更完整的例子,将上(shàng)面介紹的概念彙集在一(yī)起​​。我們假設一(yī)個購物(wù)應用程序,該應用程序顯示出售的各種産品,并維護一(yī)個購物(wù)車。 我們先來定義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 widget是無狀态的。它将其在構造函​​數(shù)中接收到的值存儲在final成員(yuán)變量中,然後在build函數(shù)中使用它們。 例如(rú),inCart布爾值表示在兩種視(shì)覺展示效果之間(jiān)切換:一(yī)個使用當前主題的主色,另一(yī)個使用灰色。

當用戶點擊列表項時(shí),widget不會直接修改其inCart的值。相反,widget會調用其父widget給它的onCartChanged回調函數(shù)。 此模式可(kě)讓您在widget層次結構中存儲更高的狀态,從(cóng)而使狀态持續更長(cháng)的時(shí)間(jiān)。在極端情況下(xià),存儲傳給runApp應用程序的widget的狀态将在的整個生命周期中持續存在。

當父項收到onCartChanged回調時(shí),父項将更新其內(nèi)部狀态,這(zhè)将觸發父項使用新inCart值重建ShoppingListItem新實例。 雖然父項ShoppingListItem在重建時(shí)創建了(le)一(yī)個新實例,但(dàn)該操作(zuò)開(kāi)銷很(hěn)小,因為(wèi)Flutter框架會将新構建的widget與先前構建的widget進行(xíng)比較,并僅将差異部分應用于底層RenderObject。

我們來看(kàn)看(kàn)父widget存儲可(kě)變狀态的示例:

class ShoppingList extends StatefulWidget {

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

final List 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 {

Set _shoppingCart = new Set();

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: [

new Product(name: 'Eggs'),

new Product(name: 'Flour'),

new Product(name: 'Chocolate chips'),

],

),

));

}

ShoppingList類繼承自(zì)StatefulWidget,這(zhè)意味着這(zhè)個widget可(kě)以存儲狀态。 當ShoppingList首次插入到樹(shù)中時(shí),框架會調用其 createState 函數(shù)以創建一(yī)個新的_ShoppingListState實例來與該樹(shù)中的相應位置關聯(請(qǐng)注意,我們通(tōng)常命名State子類時(shí)帶一(yī)個下(xià)劃線,這(zhè)表示其是私有的)。 當這(zhè)個widget的父級重建時(shí),父級将創建一(yī)個新的ShoppingList實例,但(dàn)是Flutter框架将重用已經在樹(shù)中的_ShoppingListState實例,而不是再次調用createState創建一(yī)個新的。

要(yào)訪問(wèn)當前ShoppingList的屬性,_ShoppingListState可(kě)以使用它的widget屬性。 如(rú)果父級重建并創建一(yī)個新的ShoppingList,那(nà)麽 _ShoppingListState也将用新的widget值重建(譯者語:這(zhè)裏原文檔有錯誤,應該是_ShoppingListState不會重新構建,但(dàn)其widget的屬性會更新為(wèi)新構建的widget)。 如(rú)果希望在widget屬性更改時(shí)收到通(tōng)知,則可(kě)以覆蓋didUpdateWidget函數(shù),以便将舊(jiù)的oldWidget與當前widget進行(xíng)比較。

處理onCartChanged回調時(shí),_ShoppingListState通(tōng)過添加或删除産品來改變其內(nèi)部_shoppingCart狀态。 為(wèi)了(le)通(tōng)知框架它改變了(le)它的內(nèi)部狀态,需要(yào)調用setState。調用setState将該widget标記為(wèi)”dirty”(髒的),并且計劃在下(xià)次應用程序需要(yào)更新屏幕時(shí)重新構建它。 如(rú)果在修改widget的內(nèi)部狀态後忘記調用setState,框架将不知道(dào)您的widget是”dirty”(髒的),并且可(kě)能(néng)不會調用widget的build方法,這(zhè)意味着用戶界面可(kě)能(néng)不會更新以展示新的狀态。

通(tōng)過以這(zhè)種方式管理狀态,您不需要(yào)編寫用于創建和(hé)更新子widget的單獨代碼。相反,您隻需實現可(kě)以處理這(zhè)兩種情況的build函數(shù)。

響應widget生命周期事件

主要(yào)文章(zhāng): State

在StatefulWidget調用createState之後,框架将新的狀态對象插入樹(shù)中,然後調用狀态對象的initState。 子類化State可(kě)以重寫initState,以完成僅需要(yào)執行(xíng)一(yī)次的工(gōng)作(zuò)。 例如(rú),您可(kě)以重寫initState以配置動畫(huà)或訂閱platform services。initState的實現中需要(yào)調用super.initState。

當一(yī)個狀态對象不再需要(yào)時(shí),框架調用狀态對象的dispose。 您可(kě)以覆蓋該dispose方法來執行(xíng)清理工(gōng)作(zuò)。例如(rú),您可(kě)以覆蓋dispose取消定時(shí)器或取消訂閱platform services。 dispose典型的實現是直接調用super.dispose。

Key

主要(yào)文章(zhāng): Key_

您可(kě)以使用key來控制框架将在widget重建時(shí)與哪些其他(tā)widget匹配。默認情況下(xià),框架根據它們的runtimeType和(hé)它們的顯示順序來匹配。 使用key時(shí),框架要(yào)求兩個widget具有相同的key和(hé)runtimeType。

Key在構建相同類型widget的多個實例時(shí)很(hěn)有用。例如(rú),ShoppingList構建足夠的ShoppingListItem實例以填充其可(kě)見(jiàn)區(qū)域:

如(rú)果沒有key,當前構建中的第一(yī)個條目将始終與前一(yī)個構建中的第一(yī)個條目同步,即使在語義上(shàng),列表中的第一(yī)個條目如(rú)果滾動出屏幕,那(nà)麽它将不會再在窗口中可(kě)見(jiàn)。

通(tōng)過給列表中的每個條目分配為(wèi)“語義” key,無限列表可(kě)以更高效,因為(wèi)框架将同步條目與匹配的語義key并因此具有相似(或相同)的可(kě)視(shì)外(wài)觀。 此外(wài),語義上(shàng)同步條目意味着在有狀态子widget中,保留的狀态将附加到相同的語義條目上(shàng),而不是附加到相同數(shù)字位置上(shàng)的條目。

全局 Key

主要(yào)文章(zhāng): GlobalKey

您可(kě)以使用全局key來唯一(yī)标識子widget。全局key在整個widget層次結構中必須是全局唯一(yī)的,這(zhè)與局部key不同,後者隻需要(yào)在同級中唯一(yī)。由于它們是全局唯一(yī)的,因此可(kě)以使用全局key來檢索與widget關聯的狀态。

網站建設開(kāi)發|APP設計開(kāi)發|小程序建設開(kāi)發
下(xià)一(yī)篇:Flutter Widget目錄
上(shàng)一(yī)篇:Flutter 編寫第一(yī)個Flutter應用