以下內容基本翻譯自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 Flutter 和Adding 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時,你通常會創建 StatelessWidget
或 StatefulWidget
的子類作為組件,繼承那個類取決與你的組件是否管理狀態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區分的組件、亦被稱為routes
的Navigator
。Navigator
讓你平滑在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.dart
的AppBar
和Scaffold
,我們的app看起來更加Material。例如,app bar有陰影了,title文本自動繼承了正確的樣式。我們也添加了一個合適的浮動按鈕(FloatingActionButton)。
注意,我們再次把組件作為參數傳遞給另一個組件。Scaffold
需要許多不同的組件作為參數,他們將被放在Scaffold的適當位置。類似的,AppBar
組件讓我們傳遞組件作為 title
組件的leading
和 actions
。這種模式遍布整個框架,你在設計自己的組件時也需要考慮。
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。當用戶輕擊Container
,GestureDetector
將回調它的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'),
],
);
}
}
你可能好奇,為什么StatefulWidget
和State
是分離的對象。在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),也就不會調用widget
的build
方法,這意味著,用戶界面不會更新反應狀態的改變。通過這種方式管理state,你不必分別為創建和更新子widget寫代碼,只需簡單的實現
build
方法,它會處理兩種情況。
Responding to widget lifecycle events 組件生命周期響應
Main article: State
StatefulWidget
調用 createState
之后,框架(framework)插入一個新的state對象到樹(tree)中,然后調用state對象的 initState
。State
的子類可以復寫(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檢索與組件連接狀態。