使用 Flutter 構建使用者介面
Flutter 元件採用受 React 啟發的現代框架構建。其核心思想是,你使用元件構建使用者介面。元件描述了在給定當前配置和狀態的情況下,其檢視應該是什麼樣子。當元件的狀態發生變化時,元件會重建其描述,框架會將該描述與之前的描述進行比較,以確定底層渲染樹中從一個狀態轉換到下一個狀態所需的最小更改。
Hello world
#最簡單的 Flutter 應用只需使用一個元件呼叫 runApp() 函式
import 'package:flutter/material.dart';
void main() {
runApp(
const Center(
child: Text(
'Hello, world!',
textDirection: TextDirection.ltr,
style: TextStyle(color: Colors.blue),
),
),
);
}
runApp() 函式接收給定的 Widget 並使其成為元件樹的根。在此示例中,元件樹由兩個元件組成:Center 元件及其子元件 Text 元件。框架強制根元件覆蓋螢幕,這意味著“Hello, world”文字最終會居中顯示。在此例項中需要指定文字方向;當使用 MaterialApp 元件時,這會自動為你處理,如下面所示。
在編寫應用時,你通常會根據元件是否管理任何狀態,編寫繼承自 StatelessWidget 或 StatefulWidget 的新元件。元件的主要任務是實現一個 build() 函式,該函式使用其他低階元件來描述該元件。框架會依次構建這些元件,直到最終構建代表底層 RenderObject 的元件,RenderObject 計算並描述元件的幾何形狀。
基本元件
#Flutter 帶有一套功能強大的基本元件,其中以下元件是常用的
文字Text元件允許你在應用中建立一段帶樣式的文字。Row,Column- 這些彈性元件允許你在水平 (
Row) 和垂直 (Column) 方向上建立靈活的佈局。這些物件的設計基於 Web 的 flexbox 佈局模型。 層疊佈局Stack元件不是線性定向(水平或垂直)的,它允許你按繪製順序將元件相互堆疊。然後,你可以使用Positioned元件對Stack的子元件進行定位,使其相對於堆疊的頂部、右側、底部或左側邊緣。堆疊基於 Web 的絕對定位佈局模型。容器Container元件允許你建立一個矩形的可視元素。容器可以使用BoxDecoration進行裝飾,例如背景、邊框或陰影。Container還可以應用邊距、填充和大小約束。此外,Container可以使用矩陣在三維空間中進行變換。
下面是一些結合了這些元件和其他元件的簡單元件
import 'package:flutter/material.dart';
class MyAppBar extends StatelessWidget {
const MyAppBar({required this.title, super.key});
// Fields in a Widget subclass are always marked "final".
final Widget title;
@override
Widget build(BuildContext context) {
return Container(
height: 56, // in logical pixels
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(color: Colors.blue[500]),
// Row is a horizontal, linear layout.
child: Row(
children: [
const IconButton(
icon: Icon(Icons.menu),
tooltip: 'Navigation menu',
onPressed: null, // null disables the button
),
// Expanded expands its child
// to fill the available space.
Expanded(child: title),
const IconButton(
icon: Icon(Icons.search),
tooltip: 'Search',
onPressed: null,
),
],
),
);
}
}
class MyScaffold extends StatelessWidget {
const MyScaffold({super.key});
@override
Widget build(BuildContext context) {
// Material is a conceptual piece
// of paper on which the UI appears.
return Material(
// Column is a vertical, linear layout.
child: Column(
children: [
MyAppBar(
title: Text(
'Example title',
style:
Theme.of(context) //
.primaryTextTheme
.titleLarge,
),
),
const Expanded(child: Center(child: Text('Hello, world!'))),
],
),
);
}
}
void main() {
runApp(
const MaterialApp(
title: 'My app', // used by the OS task switcher
home: SafeArea(child: MyScaffold()),
),
);
}
確保在 pubspec.yaml 檔案的 flutter 部分有一個 uses-material-design: true 條目。它允許你使用預定義的一組 Material 圖示。如果你正在使用 Material 庫,通常最好包含此行。
name: my_app
flutter:
uses-material-design: true許多 Material Design 元件需要放置在 MaterialApp 內才能正確顯示,以便繼承主題資料。因此,請使用 MaterialApp 執行應用程式。
MyAppBar 元件建立一個 Container,其高度為 56 裝置獨立畫素,左右內邊距均為 8 畫素。在容器內部,MyAppBar 使用 Row 佈局來組織其子元件。中間的子元件,即 title 元件,被標記為 Expanded,這意味著它會擴充套件以填充未被其他子元件佔用的任何剩餘可用空間。你可以有多個 Expanded 子元件,並使用 Expanded 的 flex 引數確定它們佔用可用空間的比例。
MyScaffold 元件將其子元件組織成垂直列。在列的頂部,它放置了一個 MyAppBar 例項,並嚮應用欄傳遞一個 Text 元件作為其標題。將元件作為引數傳遞給其他元件是一種強大的技術,它允許你建立可以在多種方式中重用的通用元件。最後,MyScaffold 使用 Expanded 來用其主體填充剩餘空間,主體包含一個居中的訊息。
欲瞭解更多資訊,請檢視 佈局。
使用 Material Components
#Flutter 提供了許多元件來幫助你構建遵循 Material Design 的應用。Material 應用以 MaterialApp 元件開頭,該元件在應用的根目錄構建了許多有用的元件,包括 Navigator,它管理著一堆由字串標識的元件,也稱為“路由”。Navigator 允許你在應用程式的不同螢幕之間平滑過渡。使用 MaterialApp 元件是完全可選的,但這是一個好的實踐。
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(title: 'Flutter Tutorial', home: TutorialHome()));
}
class TutorialHome extends StatelessWidget {
const TutorialHome({super.key});
@override
Widget build(BuildContext context) {
// Scaffold is a layout for
// the major Material Components.
return Scaffold(
appBar: AppBar(
leading: const IconButton(
icon: Icon(Icons.menu),
tooltip: 'Navigation menu',
onPressed: null,
),
title: const Text('Example title'),
actions: const [
IconButton(
icon: Icon(Icons.search),
tooltip: 'Search',
onPressed: null,
),
],
),
// body is the majority of the screen.
body: const Center(child: Text('Hello, world!')),
floatingActionButton: const FloatingActionButton(
tooltip: 'Add', // used by assistive technologies
onPressed: null,
child: Icon(Icons.add),
),
);
}
}
現在程式碼已經從 MyAppBar 和 MyScaffold 切換到 AppBar 和 Scaffold 元件接受多個不同的元件作為命名引數,每個元件都放置在 Scaffold 佈局中的適當位置。類似地,AppBar 元件允許你為 leading 元件以及 title 元件的 actions 傳遞元件。這種模式在整個框架中反覆出現,並且在你設計自己的元件時也應考慮。
欲瞭解更多資訊,請檢視 Material Components 元件。
處理手勢
#大多數應用程式都包含某種形式的使用者與系統的互動。構建互動式應用程式的第一步是檢測輸入手勢。透過建立一個簡單的按鈕來了解其工作原理
import 'package:flutter/material.dart';
class MyButton extends StatelessWidget {
const MyButton({super.key});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
print('MyButton was tapped!');
},
child: Container(
height: 50,
padding: const EdgeInsets.all(8),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: Colors.lightGreen[500],
),
child: const Center(child: Text('Engage')),
),
);
}
}
void main() {
runApp(
const MaterialApp(
home: Scaffold(body: Center(child: MyButton())),
),
);
}
GestureDetector 元件沒有可視表示,而是檢測使用者做出的手勢。當用戶點選 Container 時,GestureDetector 會呼叫其 onTap() 回撥,在此示例中是向控制檯列印訊息。你可以使用 GestureDetector 來檢測各種輸入手勢,包括點選、拖動和縮放。
許多元件使用 GestureDetector 為其他元件提供可選的回撥。例如,IconButton、ElevatedButton 和 FloatingActionButton 元件都有 onPressed() 回撥,當用戶點選元件時會觸發這些回撥。
欲瞭解更多資訊,請檢視 Flutter 中的手勢。
根據輸入更改元件
#到目前為止,本頁只使用了無狀態元件。無狀態元件從其父元件接收引數,並將其儲存在 final 成員變數中。當元件被要求 build() 時,它會使用這些儲存的值為其建立的元件派生新引數。
為了構建更復雜的體驗,例如以更有趣的方式響應使用者輸入,應用程式通常會攜帶一些狀態。Flutter 使用 StatefulWidgets 來捕捉這一思想。StatefulWidgets 是特殊的元件,它們知道如何生成 State 物件,然後這些物件用於儲存狀態。考慮下面這個基本示例,使用了前面提到的 ElevatedButton
import 'package:flutter/material.dart';
class Counter extends StatefulWidget {
// This class is the configuration for the state.
// It holds the values (in this case nothing) provided
// by the parent and used by the build method of the
// State. Fields in a Widget subclass are always marked
// "final".
const Counter({super.key});
@override
State<Counter> createState() => _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 you
// change _counter without calling setState(), then
// the build method won't 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 changes instances of widgets.
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(onPressed: _increment, child: const Text('Increment')),
const SizedBox(width: 16),
Text('Count: $_counter'),
],
);
}
}
void main() {
runApp(
const MaterialApp(
home: Scaffold(body: Center(child: Counter())),
),
);
}
你可能會疑惑為什麼 StatefulWidget 和 State 是獨立的兩個物件。在 Flutter 中,這兩種物件具有不同的生命週期。Widgets 是臨時物件,用於構建應用程式當前狀態的表示。另一方面,State 物件在對 build() 的呼叫之間是持久的,允許它們記住資訊。
上面的示例接受使用者輸入並直接在其 build() 方法中使用結果。在更復雜的應用程式中,元件層次結構的不同部分可能負責不同的關注點;例如,一個元件可能呈現一個複雜的使用者介面,旨在收集特定資訊,例如日期或位置,而另一個元件可能使用該資訊來改變整體呈現。
在 Flutter 中,更改通知透過回撥“向上”流經元件層次結構,而當前狀態“向下”流向進行演示的無狀態元件。重定向此流動的共同父級是 State。以下這個稍微複雜的示例展示了這在實踐中是如何工作的
import 'package:flutter/material.dart';
class CounterDisplay extends StatelessWidget {
const CounterDisplay({required this.count, super.key});
final int count;
@override
Widget build(BuildContext context) {
return Text('Count: $count');
}
}
class CounterIncrementor extends StatelessWidget {
const CounterIncrementor({required this.onPressed, super.key});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return ElevatedButton(onPressed: onPressed, child: const Text('Increment'));
}
}
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _counter = 0;
void _increment() {
setState(() {
++_counter;
});
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CounterIncrementor(onPressed: _increment),
const SizedBox(width: 16),
CounterDisplay(count: _counter),
],
);
}
}
void main() {
runApp(
const MaterialApp(
home: Scaffold(body: Center(child: Counter())),
),
);
}
請注意建立了兩個新的無狀態元件,清楚地分離了**顯示**計數器 (CounterDisplay) 和**更改**計數器 (CounterIncrementor) 的關注點。儘管最終結果與前一個示例相同,但職責分離允許在單個元件中封裝更大的複雜性,同時保持父元件的簡潔性。
欲瞭解更多資訊,請檢視
融會貫通
#接下來是一個更完整的示例,它將這些概念結合在一起:一個假想的購物應用程式顯示各種待售商品,並維護一個用於預期購買的購物車。首先定義表示類 ShoppingListItem
import 'package:flutter/material.dart';
class Product {
const Product({required this.name});
final String name;
}
typedef CartChangedCallback = void Function(Product product, bool inCart);
class ShoppingListItem extends StatelessWidget {
ShoppingListItem({
required this.product,
required this.inCart,
required this.onCartChanged,
}) : super(key: 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 const TextStyle(
color: Colors.black54,
decoration: TextDecoration.lineThrough,
);
}
@override
Widget build(BuildContext context) {
return ListTile(
onTap: () {
onCartChanged(product, inCart);
},
leading: CircleAvatar(
backgroundColor: _getColor(context),
child: Text(product.name[0]),
),
title: Text(product.name, style: _getTextStyle(context)),
);
}
}
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: ShoppingListItem(
product: const Product(name: 'Chips'),
inCart: true,
onCartChanged: (product, inCart) {},
),
),
),
),
);
}
ShoppingListItem 元件遵循無狀態元件的常見模式。它將其在建構函式中接收到的值儲存在 final 成員變數中,然後在 build() 函式中使用這些值。例如,inCart 布林值在兩種視覺外觀之間切換:一種使用當前主題的主色,另一種使用灰色。
當用戶點選列表項時,元件不會直接修改其 inCart 值。相反,元件呼叫它從其父元件接收到的 onCartChanged 函式。這種模式允許你將狀態儲存在元件層次結構中較高的位置,從而使狀態持續更長的時間。在極端情況下,儲存在傳遞給 runApp() 的元件上的狀態在應用程式的整個生命週期中都存在。
當父元件收到 onCartChanged 回撥時,父元件會更新其內部狀態,這會觸發父元件重建並建立具有新 inCart 值的新 ShoppingListItem 例項。儘管父元件在重建時會建立一個新的 ShoppingListItem 例項,但該操作成本很低,因為框架會將新構建的元件與之前構建的元件進行比較,並且只將差異應用於底層 RenderObject。
這是一個儲存可變狀態的父元件示例
import 'package:flutter/material.dart';
class Product {
const Product({required this.name});
final String name;
}
typedef CartChangedCallback = void Function(Product product, bool inCart);
class ShoppingListItem extends StatelessWidget {
ShoppingListItem({
required this.product,
required this.inCart,
required this.onCartChanged,
}) : super(key: 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 const TextStyle(
color: Colors.black54,
decoration: TextDecoration.lineThrough,
);
}
@override
Widget build(BuildContext context) {
return ListTile(
onTap: () {
onCartChanged(product, inCart);
},
leading: CircleAvatar(
backgroundColor: _getColor(context),
child: Text(product.name[0]),
),
title: Text(product.name, style: _getTextStyle(context)),
);
}
}
class ShoppingList extends StatefulWidget {
const ShoppingList({required this.products, super.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 re-uses
// the State object instead of creating a new State object.
@override
State<ShoppingList> createState() => _ShoppingListState();
}
class _ShoppingListState extends State<ShoppingList> {
final _shoppingCart = <Product>{};
void _handleCartChanged(Product product, bool inCart) {
setState(() {
// When a user changes what's in the cart, you 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 Scaffold(
appBar: AppBar(title: const Text('Shopping List')),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: widget.products.map((product) {
return ShoppingListItem(
product: product,
inCart: _shoppingCart.contains(product),
onCartChanged: _handleCartChanged,
);
}).toList(),
),
);
}
}
void main() {
runApp(
const MaterialApp(
title: 'Shopping App',
home: ShoppingList(
products: [
Product(name: 'Eggs'),
Product(name: 'Flour'),
Product(name: 'Chocolate chips'),
],
),
),
);
}
ShoppingList 類擴充套件了 StatefulWidget,這意味著此元件儲存可變狀態。當 ShoppingList 元件首次插入到樹中時,框架會呼叫 createState() 函式以建立 _ShoppingListState 的新例項,並將其與樹中的該位置關聯。(請注意,State 的子類通常以開頭的下劃線命名,表示它們是私有實現細節。)當此元件的父元件重建時,父元件會建立一個新的 ShoppingList 例項,但框架會重用樹中已有的 _ShoppingListState 例項,而不是再次呼叫 createState。
要訪問當前 ShoppingList 的屬性,_ShoppingListState 可以使用其 widget 屬性。如果父元件重建並建立新的 ShoppingList,_ShoppingListState 將使用新的元件值進行重建。如果你希望在 widget 屬性更改時收到通知,請重寫 didUpdateWidget() 函式,該函式會傳入一個 oldWidget,以便你可以比較舊元件與當前元件。
在處理 onCartChanged 回撥時,_ShoppingListState 透過向 _shoppingCart 新增或移除產品來修改其內部狀態。為了向框架表明其內部狀態已更改,它將這些呼叫包裝在 setState() 呼叫中。呼叫 setState 會將此元件標記為髒,並安排它在應用程式下次需要更新螢幕時重建。如果你在修改元件內部狀態時忘記呼叫 setState,框架將不知道你的元件已髒,並且可能不會呼叫元件的 build() 函式,這意味著使用者介面可能不會更新以反映更改的狀態。透過這種方式管理狀態,你無需為建立和更新子元件編寫單獨的程式碼。相反,你只需實現 build 函式,它會處理這兩種情況。
響應元件生命週期事件
#在 StatefulWidget 上呼叫 createState() 後,框架會將新的狀態物件插入樹中,然後呼叫狀態物件的 initState()。State 的子類可以重寫 initState 來執行只需要發生一次的工作。例如,重寫 initState 以配置動畫或訂閱平臺服務。initState 的實現必須先呼叫 super.initState。
當不再需要狀態物件時,框架會呼叫狀態物件的 dispose()。重寫 dispose 函式來執行清理工作。例如,重寫 dispose 以取消計時器或取消訂閱平臺服務。dispose 的實現通常以呼叫 super.dispose 結束。
欲瞭解更多資訊,請檢視 State。
鍵(Keys)
#當元件重建時,使用鍵來控制框架將哪些元件與其他元件匹配。預設情況下,框架根據它們的 runtimeType 以及它們出現的順序來匹配當前和之前構建中的元件。使用鍵,框架要求兩個元件具有相同的 key 以及相同的 runtimeType。
鍵在構建許多相同型別元件例項的元件中最有用。例如,ShoppingList 元件,它只構建足夠的 ShoppingListItem 例項來填充其可見區域
如果沒有鍵,當前構建中的第一個條目將始終與之前構建中的第一個條目同步,即使從語義上講,列表中的第一個條目只是滾出螢幕並且在視口中不再可見。
透過為列表中的每個條目分配一個“語義”鍵,無限列表可以更高效,因為框架會同步具有匹配語義鍵的條目,因此具有相似(或相同)的視覺外觀。此外,語義同步條目意味著有狀態子元件中保留的狀態仍然附加到相同的語義條目,而不是視口中相同數字位置的條目。
欲瞭解更多資訊,請檢視 Key API。
全域性鍵(Global keys)
#使用全域性鍵(global keys)來唯一標識子元件。全域性鍵必須在整個元件層次結構中全域性唯一,與區域性鍵不同,區域性鍵只需在兄弟元件之間唯一。由於它們是全域性唯一的,因此可以使用全域性鍵來檢索與元件關聯的狀態。
欲瞭解更多資訊,請檢視 GlobalKey API。