為你的 Flutter 應用加入互動體驗

如何修改您的應用程式以使其對使用者輸入做出反應?在本課程中,您將為僅包含非互動式 widget 的應用程式新增互動性。具體來說,您將透過建立一個管理兩個無狀態 widget 的自訂有狀態 widget,修改一個圖示實現使其可點選。

建構佈局課程 中展示瞭如何建構下面截圖所示的佈局。

The layout tutorial app
The layout tutorial app

當應用第一次啟動時,這個星形圖示是實心紅色,表明這個湖以前已經被收藏過了。星號旁邊的數字表示 41 個人已經收藏了此湖。完成本課程後,點選星形圖示將取消收藏狀態,然後用輪廓線的星形圖示代替實心的,並減少計數。再次點選會重新收藏,並增加計數。

The custom widget you'll create

為了實現這個,您將建立一個包含星形圖示和計數的自訂 widget,它們都是 widget。因為點選星形圖示會更改這兩個 widget 的狀態,所以同一個 widget 應該同時管理這兩個 widget。

您可以直接檢視 第二步: 建立 StatefulWidget 的子類別。如果您想嘗試不同的管理狀態方式,請跳至 狀態管理

有狀態和無狀態的 widgets

有些 widgets 是有狀態的, 有些是無狀態的。如果使用者與 widget 互動,widget 會發生變化,那麼它就是 有狀態的

無狀態的 widget 自身無法改變。 IconIconButtonText 都是無狀態 widget,它們都是 StatelessWidget 的子類別。

有狀態的 widget 自身是可動態改變的(基於State)。例如,可以透過與使用者的互動或是隨著資料的改變而導致外觀形態的變化。 CheckboxRadioSliderInkWellFormTextField 都是有狀態 widget,它們都是 StatefulWidget 的子類別。

一個 widget 的狀態儲存在一個 State 物件中,它和 widget 的顯示分離。 Widget 的狀態是一些可以更改的值,如一個滑動條的當前值或一個複選框是否被選中。當 widget 狀態改變時,State 物件呼叫 setState(),告訴框架去重繪 widget。

建立一個有狀態的 widget

在本節中,您將建立一個自訂有狀態的 widget。您將使用一個自訂有狀態 widget 來替換兩個無狀態 widget—— 紅色實心星形圖示和其旁邊的數字計數—— 該 widget 用兩個子 widget 管理一行 IconButtonText

實現一個自訂的有狀態 widget 需要建立兩個類:

  • 一個 StatefulWidget 的子類別,用來定義一個 widget 類別。

  • 一個 State 的子類別,包含該widget狀態並定義該 widget 的 build() 方法。

這一節展示如何為 Lakes 應用程式建構一個名為 FavoriteWidget 的 StatefulWidget。第一步是選擇如何管理 FavoriteWidget 的狀態。

步驟 0: 開始

如果你已經在 建構佈局課程(第 6 步) 中成功建立了應用程式,你可以跳過下面的部分。

  1. 確保你已經 設定 好了你的環境。

  2. 建立一個新的 Flutter 應用

  3. 用 GitHub 上的 main.dart 替換 lib/main.dart 檔案。

  4. 用 GitHub 上的 pubspec.yaml 替換 pubspec.yaml 檔案。

  5. 在你的工程中建立一個 images 資料夾,並新增 lake.jpg

如果你有一個連線並可用的裝置,或者你已經啟動了 iOS 模擬器 或者 Android 模擬器 (Flutter 安裝部分介紹過),你就可以開始了!

Step 1: 決定哪個物件管理 widget 的狀態

一個 widget 的狀態可以透過多種方式進行管理,但在我們的範例中,widget 本身 ——FavoriteWidget—— 將管理自己的狀態。在這個例子中,切換星形圖示是一個獨立的操作,不會影響父視窗 widget 或其他使用者介面,因此該 widget 可以在內部處理它自己的狀態。

你可以在 狀態管理 中瞭解更多關於 widget 和狀態的分離以及如何管理狀態的資訊。

Step 2: 建立 StatefulWidget 的子類別

FavoriteWidget 類管理自己的狀態,因此它透過重寫 createState() 來建立狀態物件。框架會在建構 widget 時呼叫 createState()。在這個例子中,createState() 建立 _FavoriteWidgetState 的例項,您將在下一步中實現該例項。

lib/main.dart (FavoriteWidget)
class FavoriteWidget extends StatefulWidget {
  const FavoriteWidget({super.key});

  @override
  State<FavoriteWidget> createState() => _FavoriteWidgetState();
}

Step 3: 建立 State 的子類別

_FavoriteWidgetState 類儲存可變資訊;可以在 widget 的生命週期內改變邏輯和內部狀態。當應用第一次啟動時,使用者介面顯示一個紅色實心的星星形圖示,表明該湖已經被收藏,並有 41 個「喜歡」。狀態物件儲存這些資訊在 _isFavorited_favoriteCount 變數中。

lib/main.dart (_FavoriteWidgetState fields)
class _FavoriteWidgetState extends State<FavoriteWidget> {
  bool _isFavorited = true;
  int _favoriteCount = 41;

  // ···
}

狀態物件也定義了 build() 方法。這個 build() 方法建立一個包含紅色 IconButtonText 的行。該 widget 使用 IconButton(而不是 Icon),因為它具有一個 onPressed 屬性,該屬性定義了處理點選的回呼(Callback)方法 (_toggleFavorite)。你將會在接下來的步驟中嘗試定義它。

lib/main.dart (_FavoriteWidgetState build)
class _FavoriteWidgetState extends State<FavoriteWidget> {
  // ···
  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          padding: const EdgeInsets.all(0),
          child: IconButton(
            padding: const EdgeInsets.all(0),
            alignment: Alignment.centerRight,
            icon: (_isFavorited
                ? const Icon(Icons.star)
                : const Icon(Icons.star_border)),
            color: Colors.red[500],
            onPressed: _toggleFavorite,
          ),
        ),
        SizedBox(
          width: 18,
          child: SizedBox(
            child: Text('$_favoriteCount'),
          ),
        ),
      ],
    );
  }
}

按下 IconButton 時會呼叫 _toggleFavorite() 方法,然後它會呼叫 setState()。呼叫 setState() 是至關重要的,因為這告訴框架, widget 的狀態已經改變,應該重繪。 setState() 在如下兩種狀態中切換 UI:

  • 實心的星形圖示和數字 41

  • 輪廓線的星形圖示 star_border 和數字 40 之間切換 UI

void _toggleFavorite() {
  setState(() {
    if (_isFavorited) {
      _favoriteCount -= 1;
      _isFavorited = false;
    } else {
      _favoriteCount += 1;
      _isFavorited = true;
    }
  });
}

Step 4: 將有 stateful widget 插入 widget 樹中

將您自訂 stateful widget 在 build() 方法中新增到 widget 樹中。首先,找到建立 IconText 的程式碼,並刪除它,在相同的位置建立有狀態的 widget:

layout/lakes/{step6 → interactive}/lib/main.dart
@@ -10,2 +5,2 @@
10
5
  class MyApp extends StatelessWidget {
11
6
  const MyApp({super.key});
@@ -40,11 +35,7 @@
40
35
  ],
41
36
  ),
42
37
  ),
43
- Icon(
44
- Icons.star,
45
- color: Colors.red[500],
46
- ),
47
- const Text('41'),
38
+ const FavoriteWidget(),
48
39
  ],
49
40
  ),
50
41
  );

就是這樣!當您熱重載應用後,星形圖示就會響應點選了。

有問題?

如果您的程式碼無法執行,請在 IDE 中查詢可能的錯誤。 除錯 Flutter 應用程式 可能會有所幫助。如果仍然無法找到問題,請根據 GitHub 上的範例檢查程式碼。

如果您仍有問題,可以諮詢 社群 中的任何一位開發者。


本頁面的其餘部分介紹了可以管理 widget 狀態的幾種方式,並列出了其他可用的可互動的 widget。

狀態管理

誰管理著 stateful widget 的狀態?widget 本身?父 widget?雙方?另一個物件?答案是…… 這取決於實際情況。有幾種有效的方法可以給你的 widget 加入互動。作為 widget 設計師,你可以基於你所期待的表現 widget 的方式來做決定。以下是一些管理狀態的最常見的方法:

如何決定使用哪種管理方法?以下原則可以幫助您決定:

  • 如果狀態是使用者資料,如複選框的選中狀態、滑塊的位置,則該狀態最好由父 widget 管理。

  • 如果所討論的狀態是有關介面外觀效果的,例如動畫,那麼狀態最好由 widget 本身來管理。

如果有疑問,首選是在父 widget 中管理狀態。

我們將透過建立三個簡單範例來舉例說明管理狀態的不同方式: TapboxA、TapboxB 和 TapboxC。這些例子功能是相似的—— 每建立一個容器,當點選時,在綠色或灰色框之間切換。 _active 確定顏色:綠色為 true,灰色為 false。

Active state Inactive state

這些範例使用 GestureDetector 捕獲 Container 上的使用者動作。

widget 管理自己的狀態

有時,widget 在內部管理其狀態是最好的。例如,當 ListView 的內容超過渲染框時, ListView 自動滾動。大多數使用 ListView 的開發人員不想管理 ListView 的滾動行為,因此 ListView 本身管理其滾動偏移量。

_TapboxAState 類:

  • 管理 TapboxA 的狀態。

  • 定義布林值 _active 確定盒子的當前顏色。

  • 定義 _handleTap() 函式,該函式在點選該盒子時更新 _active,並呼叫 setState() 更新 UI。

  • 實現 widget 的所有互動式行為。

import 'package:flutter/material.dart';

// TapboxA manages its own state.

//------------------------- TapboxA ----------------------------------

class TapboxA extends StatefulWidget {
  const TapboxA({super.key});

  @override
  State<TapboxA> createState() => _TapboxAState();
}

class _TapboxAState extends State<TapboxA> {
  bool _active = false;

  void _handleTap() {
    setState(() {
      _active = !_active;
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
        child: Center(
          child: Text(
            _active ? 'Active' : 'Inactive',
            style: const TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
      ),
    );
  }
}

//------------------------- MyApp ----------------------------------

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter Demo'),
        ),
        body: const Center(
          child: TapboxA(),
        ),
      ),
    );
  }
}

父 widget 管理 widget 的 state

一般來說父 widget 管理狀態並告訴其子 widget 何時更新通常是最合適的。例如,IconButton 允許您將圖示視為可點按的按鈕。 IconButton 是一個無狀態 widget,因為我們認為父 widget 需要知道該按鈕是否被點選來採取相應的處理。

在以下範例中,TapboxB 透過回呼(Callback)將其狀態到其父類別。由於 TapboxB 不管理任何狀態,因此它繼承自 StatelessWidget。

ParentWidgetState 類:

  • 為 TapboxB 管理 _active 狀態;

  • 實現 _handleTapboxChanged(),當盒子被點選時呼叫的方法;

  • 當狀態改變時,呼叫 setState() 更新 UI。

TapboxB 類:

  • 繼承 StatelessWidget 類,因為所有狀態都由其父 widget 處理;

  • 當檢測到點選時,它會通知父 widget。

import 'package:flutter/material.dart';

// ParentWidget manages the state for TapboxB.

//------------------------ ParentWidget --------------------------------

class ParentWidget extends StatefulWidget {
  const ParentWidget({super.key});

  @override
  State<ParentWidget> createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      child: TapboxB(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//------------------------- TapboxB ----------------------------------

class TapboxB extends StatelessWidget {
  const TapboxB({
    super.key,
    this.active = false,
    required this.onChanged,
  });

  final bool active;
  final ValueChanged<bool> onChanged;

  void _handleTap() {
    onChanged(!active);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
        child: Center(
          child: Text(
            active ? 'Active' : 'Inactive',
            style: const TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
      ),
    );
  }
}

混搭管理

對於一些 widget 來說,混搭管理的方法最合適的。在這種情況下,有狀態的 widget 自己管理一些狀態,同時父 widget 管理其他方面的狀態。

TapboxC 範例中,點選時,盒子的周圍會出現一個深綠色的邊框。點選時,邊框消失,盒子的顏色改變。 TapboxC 將其 _active 狀態匯出到其父 widget 中,但在內部管理其 _highlight 狀態。這個例子有兩個狀態物件 _ParentWidgetState_TapboxCState

_ParentWidgetState 物件:

  • 管理_active 狀態。

  • 實現 _handleTapboxChanged(),此方法在盒子被點選時呼叫。

  • 當點選盒子並且 _active 狀態改變時呼叫 setState() 來更新 UI。

_TapboxCState 物件:

  • 管理 _highlight state。

  • GestureDetector 監聽所有 tap 事件。當用戶點下時,它新增高亮(深綠色邊框);當用戶釋放時,會移除高亮。

  • 當按下、抬起、或者取消點選時更新 _highlight 狀態,呼叫 setState() 更新UI。

  • 當點選時,widget 屬性將狀態的改變傳遞給父 widget 並進行合適的操作。

import 'package:flutter/material.dart';

//---------------------------- ParentWidget ----------------------------

class ParentWidget extends StatefulWidget {
  const ParentWidget({super.key});

  @override
  State<ParentWidget> createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      child: TapboxC(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//----------------------------- TapboxC ------------------------------

class TapboxC extends StatefulWidget {
  const TapboxC({
    super.key,
    this.active = false,
    required this.onChanged,
  });

  final bool active;
  final ValueChanged<bool> onChanged;

  @override
  State<TapboxC> createState() => _TapboxCState();
}

class _TapboxCState extends State<TapboxC> {
  bool _highlight = false;

  void _handleTapDown(TapDownDetails details) {
    setState(() {
      _highlight = true;
    });
  }

  void _handleTapUp(TapUpDetails details) {
    setState(() {
      _highlight = false;
    });
  }

  void _handleTapCancel() {
    setState(() {
      _highlight = false;
    });
  }

  void _handleTap() {
    widget.onChanged(!widget.active);
  }

  @override
  Widget build(BuildContext context) {
    // This example adds a green border on tap down.
    // On tap up, the square changes to the opposite state.
    return GestureDetector(
      onTapDown: _handleTapDown, // Handle the tap events in the order that
      onTapUp: _handleTapUp, // they occur: down, up, tap, cancel
      onTap: _handleTap,
      onTapCancel: _handleTapCancel,
      child: Container(
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
          border: _highlight
              ? Border.all(
                  color: Colors.teal[700]!,
                  width: 10.0,
                )
              : null,
        ),
        child: Center(
          child: Text(widget.active ? 'Active' : 'Inactive',
              style: const TextStyle(fontSize: 32.0, color: Colors.white)),
        ),
      ),
    );
  }
}

另一種實現可能會將高亮狀態匯出到父 widget,同時保持 active 狀態為內部,但如果你要求某人使用該 TapBox,他們可能會抱怨說沒有多大意義。開發人員只會關心該框是否處於活動狀態。開發人員可能不在乎高亮顯示是如何管理的,並且傾向於讓 TapBox 處理這些細節。


其他互動式 widgets

Flutter 提供各種按鈕和類似的互動式 widget。這些 widget 中的大多數都實現了 Material Design guidelines,它們定義了一組具有質感的 UI 元件。

如果你願意,你可以使用 GestureDetector 來給任何自訂 widget 新增互動性。你可以在 管理狀態 中找到 GestureDetector 的範例。同時你也可以在 Flutter cookbook處理點選 中學習更多關於 GestureDetector 的內容。

當你需要互動性時,最容易的是使用預製的 widget。這是預置 widget 部分列表:

標準 widgets

Material 元件

資源

以下資源可能會在給您的應用新增互動的時候有所幫助。

手勢Flutter 實用課程 裡的一個小節。

處理手勢widgets 介紹 文件中一部分
如何建立一個按鈕並使其響應使用者動作。

點選、拖動和其他手勢
Flutter 手勢機制的描述。

Flutter API 文件
所有 Flutter API 的參考文件。

Flutter Gallery 應用,程式碼儲存庫
一個 Demo 應用程式,展示了許多 Material 和其他 Flutter 功能。

Flutter 的分層設計 (影片)
此影片包含有關有狀態和無狀態 widget 的資訊。由 Google 工程師 Ian Hickson 講解。