使用 Flutter 建置使用者介面
Flutter Widget 是使用現代架構建置的,該架構的靈感來自 React。核心概念是您使用 Widget 建置 UI。Widget 會描述在目前設定和狀態下,其檢視畫面應呈現的外觀。當 Widget 的狀態變更時,Widget 會重建其描述,而架構會將其與先前的描述進行差異比較,以判斷從一個狀態轉換到下一個狀態時,基礎轉譯樹中需要進行的最小變更。
Hello world
#最簡化的 Flutter 應用程式只需使用一個小工具呼叫 runApp()
函數
import 'package:flutter/material.dart';
void main() {
runApp(
const Center(
child: Text(
'Hello, world!',
textDirection: TextDirection.ltr,
),
),
);
}
runApp()
函數會接收給定的 Widget
,並將其設為小工具樹狀結構的根。在此範例中,小工具樹狀結構由兩個小工具組成,Center
小工具及其子小工具 Text
小工具。架構會強制根小工具覆蓋螢幕,這表示文字 "Hello, world" 最後會置於螢幕中央。在此情況下需要指定文字方向;當使用 MaterialApp
小工具時,系統會為您處理此問題,稍後會示範。
在編寫應用程式時,您通常會撰寫屬於 StatelessWidget
或 StatefulWidget
子類別的新小工具,具體取決於您的小工具是否管理任何狀態。小工具的主要工作是實作 build()
函數,此函數會以其他較低層級的小工具來描述小工具。架構會依序建構這些小工具,直到此程序以代表底層 RenderObject
的小工具結束,RenderObject
會計算和描述小工具的幾何形狀。
基本 Widget
#Flutter 隨附一套強大的基本小工具,以下是常用的:
文字
Text
小工具可讓您在應用程式中建立一段具樣式的文字。Row
、Column
- 這些彈性小工具可讓您在水平 (
Row
) 和垂直 (Column
) 方向建立彈性的版面配置。這些物件的設計基於網頁的 flexbox 版面配置模型。 Stack
Stack
小工具不會以線性方式定向 (水平或垂直),而是可讓您依繪製順序將小工具彼此疊放。然後,您可以在Stack
的子物件上使用Positioned
小工具,以將它們相對於堆疊的頂部、右側、底部或左側邊緣定位。堆疊基於網頁的絕對定位版面配置模型。Container
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 圖示集。如果您使用 Materials 程式庫,通常最好包含此行。
name: my_app
flutter:
uses-material-design: true
許多 Material Design 小工具需要位於 MaterialApp
內才能正確顯示,以便繼承主題資料。因此,請使用 MaterialApp
執行應用程式。
MyAppBar
小工具會建立一個高度為 56 個與裝置無關像素的 Container
,其內部左側和右側都有 8 像素的內距。在容器內部,MyAppBar
會使用 Row
版面配置來組織其子物件。中間的子物件 title
小工具會標記為 Expanded
,這表示它會展開以填滿其他子物件尚未使用的任何剩餘可用空間。您可以有多個 Expanded
子物件,並使用 Expanded
的 flex
引數來決定它們消耗可用空間的比例。
MyScaffold
小工具會將其子物件組織成垂直欄。在欄的頂端,它會放置 MyAppBar
的執行個體,並將 Text
小工具傳遞至應用程式列,以作為其標題。將小工具作為引數傳遞至其他小工具是一種強大的技術,可讓您建立可在各種方式中重複使用的通用小工具。最後,MyScaffold
會使用 Expanded
以剩餘空間填滿其主體,其中包含置中的訊息。
如需更多資訊,請查看版面配置。
使用 Material 元件
#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
小工具,並從 material.dart
切換,應用程式開始看起來更像 Material。例如,應用程式列具有陰影,且標題文字會自動繼承正確的樣式。也會新增浮動動作按鈕。
請注意,小工具會以引數形式傳遞至其他小工具。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 中的手勢。
變更 Widget 以回應輸入
#到目前為止,此頁面僅使用無狀態小工具。無狀態小工具會從其父小工具接收引數,並將其儲存在 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(),
),
),
),
);
}
請注意,這裡建立了兩個新的無狀態 widget,清楚地將顯示計數器 (CounterDisplay
) 和變更計數器 (CounterIncrementor
) 的職責分開。雖然最終結果與之前的範例相同,但職責的分離讓個別 widget 可以封裝更複雜的功能,同時保持父 widget 的簡潔性。
更多資訊,請查看
整合所有功能
#接下來是一個更完整的範例,它將這些概念結合在一起:一個假設的購物應用程式會顯示販售的各種產品,並維護一個用於預計購買的購物車。首先定義呈現類別,ShoppingListItem
import 'package:flutter/material.dart';
class Product {
const Product({required this.name});
final String name;
}
typedef CartChangedCallback = 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
widget 遵循無狀態 widget 的通用模式。它將從其建構函式接收到的值儲存在 final
成員變數中,然後在它的 build()
函式中使用這些變數。例如,inCart
布林值會在兩種視覺外觀之間切換:一種使用目前主題的主要顏色,另一種則使用灰色。
當使用者點擊列表項目時,widget 不會直接修改其 inCart
值。相反地,widget 會呼叫它從父 widget 接收到的 onCartChanged
函式。這種模式讓您可以將狀態儲存在 widget 階層的較高層級,這會導致狀態持續更長的時間。在極端情況下,儲存在傳遞給 runApp()
的 widget 上的狀態會在應用程式的整個生命週期中持續存在。
當父 widget 收到 onCartChanged
回呼時,父 widget 會更新其內部狀態,這會觸發父 widget 重建並建立具有新 inCart
值的新 ShoppingListItem
實例。雖然父 widget 在重建時會建立 ShoppingListItem
的新實例,但該操作成本很低,因為框架會將新建立的 widget 與先前建立的 widget 進行比較,並且僅將差異應用於底層的 RenderObject
。
以下是一個範例父 widget,它儲存可變狀態
import 'package:flutter/material.dart';
class Product {
const Product({required this.name});
final String name;
}
typedef CartChangedCallback = 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
,這表示此 widget 儲存可變狀態。當 ShoppingList
widget 首次插入樹狀結構時,框架會呼叫 createState()
函式,以建立 _ShoppingListState
的全新實例,並將其與樹狀結構中的該位置建立關聯。(請注意,State
的子類別通常以底線開頭命名,表示它們是私有的實作細節。)當此 widget 的父 widget 重建時,父 widget 會建立新的 ShoppingList
實例,但框架會重複使用樹狀結構中已有的 _ShoppingListState
實例,而不會再次呼叫 createState
。
為了存取目前 ShoppingList
的屬性,_ShoppingListState
可以使用其 widget
屬性。如果父 widget 重建並建立新的 ShoppingList
,則 _ShoppingListState
會使用新的 widget 值重建。如果您希望在 widget
屬性變更時收到通知,請覆寫 didUpdateWidget()
函式,該函式會傳遞 oldWidget
,讓您可以將舊 widget 與目前的 widget 進行比較。
在處理 onCartChanged
回呼時,_ShoppingListState
會透過從 _shoppingCart
新增或移除產品來修改其內部狀態。為了向框架發出訊號,表明其內部狀態已變更,它會將這些呼叫包裝在 setState()
呼叫中。呼叫 setState
會將此 widget 標示為髒,並排定時間在您的應用程式下次需要更新螢幕時重建它。如果您在修改 widget 的內部狀態時忘記呼叫 setState
,框架將不知道您的 widget 是髒的,並且可能不會呼叫 widget 的 build()
函式,這表示使用者介面可能不會更新以反映變更的狀態。透過以這種方式管理狀態,您不需要編寫單獨的程式碼來建立和更新子 widget。相反地,您只需實作 build
函式,它會處理這兩種情況。
回應 Widget 生命周期事件
#在對 StatefulWidget
呼叫 createState()
之後,框架會將新的狀態物件插入樹狀結構中,然後在狀態物件上呼叫 initState()
。 State
的子類別可以覆寫 initState
以執行只需執行一次的工作。例如,覆寫 initState
以設定動畫或訂閱平台服務。initState
的實作必須從呼叫 super.initState
開始。
當不再需要狀態物件時,框架會在狀態物件上呼叫 dispose()
。覆寫 dispose
函式以執行清除工作。例如,覆寫 dispose
以取消計時器或取消訂閱平台服務。dispose
的實作通常以呼叫 super.dispose
結束。
更多資訊,請查看 State
。
Key
#使用鍵來控制框架在 widget 重建時將哪些 widget 與其他 widget 進行比對。預設情況下,框架會根據它們的 runtimeType
和它們出現的順序,比對目前和先前版本中的 widget。使用鍵,框架要求兩個 widget 除了具有相同的 runtimeType
外,還必須具有相同的 key
。
鍵在建立同一類型 widget 的多個實例的 widget 中最有用。例如,ShoppingList
widget,它只建立足夠多的 ShoppingListItem
實例來填滿其可見區域
如果沒有鍵,目前版本中的第一個項目總是會與先前版本中的第一個項目同步,即使在語義上,列表中的第一個項目只是滾動出螢幕,並且在可視區域中不再可見。
透過為列表中的每個項目指派一個「語義」鍵,無限列表可以更有效率,因為框架會將具有相符語義鍵的項目同步,因此具有相似(或相同)的視覺外觀。此外,以語義方式同步項目意味著保留在狀態子 widget 中的狀態會保持附加到相同的語義項目,而不是在可視區域中相同數字位置的項目。
更多資訊,請查看 Key
API。
全域 Key
#使用全域鍵來唯一識別子 widget。全域鍵在整個 widget 階層中必須是全域唯一的,與只需要在兄弟姐妹之間唯一性的本地鍵不同。由於它們是全域唯一的,因此可以使用全域鍵來擷取與 widget 相關聯的狀態。
更多資訊,請查看 GlobalKey
API。
除非另有說明,否則本網站上的文件反映了 Flutter 的最新穩定版本。頁面上次更新時間為 2024-06-26。 檢視原始碼 或 回報問題。