跳到主內容

為你的 Flutter 應用新增互動性

如何實現響應點選的有狀態元件。

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

構建佈局教程》向你展示瞭如何建立以下螢幕截圖的佈局。

The layout tutorial app

佈局教程應用

當應用首次啟動時,星星是純紅色,表明該湖已被收藏。星星旁邊的數字表明有 41 人收藏了該湖。完成本教程後,點選星星將移除其收藏狀態,將純星星替換為輪廓並減少計數。再次點選將收藏該湖,繪製純星星並增加計數。

The custom widget you'll create

為了實現這一點,你將建立一個單一的自定義元件,其中包含星星和計數,它們本身也是元件。點選星星會改變兩個元件的狀態,因此應該由同一個元件管理兩者。

你可以直接進入 步驟 2:繼承 StatefulWidget 開始編寫程式碼。如果你想嘗試不同的狀態管理方式,請跳到 管理狀態

有狀態和無狀態元件

#

一個元件要麼是有狀態的,要麼是無狀態的。如果一個元件可以改變——例如,當用戶與其互動時——那麼它是有狀態的。

一個無狀態元件永遠不會改變。IconIconButtonText 是無狀態元件的例子。無狀態元件繼承自 StatelessWidget

一個有狀態元件是動態的:例如,它可以根據使用者互動觸發的事件或接收到資料時改變其外觀。CheckboxRadioSliderInkWellFormTextField 是有狀態元件的例子。有狀態元件繼承自 StatefulWidget

元件的狀態儲存在一個 State 物件中,將元件的狀態與其外觀分離。狀態由可以改變的值組成,例如滑塊的當前值或複選框是否被選中。當元件的狀態改變時,狀態物件呼叫 setState(),告訴框架重新繪製元件。

建立有狀態元件

#

在本節中,你將建立一個自定義有狀態元件。你將用一個單一的自定義有狀態元件替換兩個無狀態元件——純紅色星星和星星旁邊的數字計數——該元件管理一個包含兩個子元件的行:一個 IconButton 和一個 Text

實現自定義有狀態元件需要建立兩個類

  • 一個繼承自 StatefulWidget 的子類,用於定義元件。
  • 一個繼承自 State 的子類,包含該元件的狀態並定義元件的 build() 方法。

本節展示瞭如何為湖泊應用構建一個名為 FavoriteWidget 的有狀態元件。設定好後,你的第一步是選擇如何管理 FavoriteWidget 的狀態。

步驟 0:準備就緒

#

如果你已經在《構建佈局教程》中構建了該應用,請跳到下一節。

  1. 確保你已經 設定 了你的環境。
  2. 建立一個新的 Flutter 應用.
  3. main.dart 替換 lib/main.dart 檔案。
  4. pubspec.yaml 替換 pubspec.yaml 檔案。
  5. 在你的專案中建立一個 images 目錄,並新增 lake.jpg

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

步驟 1:確定哪個物件管理元件的狀態

#

狀態管理有多種方式,但在我們的例子中,元件本身,FavoriteWidget,將管理自己的狀態。在本例中,切換星星是一個孤立的操作,不會影響父元件或 UI 的其餘部分,因此元件可以內部處理其狀態。

更多關於元件和狀態分離以及狀態管理方式的資訊,請參閱 管理狀態

步驟 2:繼承 StatefulWidget

#

FavoriteWidget 類管理自己的狀態,因此它重寫 createState() 以建立一個 State 物件。當框架想要構建元件時,會呼叫 createState()。在本例中,createState() 返回一個 _FavoriteWidgetState 的例項,你將在下一步中實現它。

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

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

步驟 3:繼承 State

#

_FavoriteWidgetState 類儲存可以在元件生命週期內改變的可變資料。當應用首次啟動時,UI 顯示一個純紅色星星,表明該湖具有“收藏”狀態,以及 41 個點贊。這些值儲存在 _isFavorited_favoriteCount 欄位中

dart
class _FavoriteWidgetState extends State<FavoriteWidget> {
  bool _isFavorited = true;
  int _favoriteCount = 41;

該類還定義了一個 build() 方法,該方法建立一個包含紅色 IconButtonText 的行。你使用 IconButton(而不是 Icon),因為它具有一個 onPressed 屬性,該屬性定義了處理點選的回撥函式(_toggleFavorite)。你將在下一步定義回撥函式。

dart
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.center,
            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() 至關重要,因為這告訴框架元件的狀態已更改,並且應該重新繪製元件。setState() 的函式引數在以下兩種狀態之間切換

  • 一個 star 圖示和數字 41
  • 一個 star_border 圖示和數字 40
dart
void _toggleFavorite() {
  setState(() {
    if (_isFavorited) {
      _favoriteCount -= 1;
      _isFavorited = false;
    } else {
      _favoriteCount += 1;
      _isFavorited = true;
    }
  });
}

步驟 4:將有狀態元件插入到元件樹中

#

將你的自定義有狀態元件新增到應用 build() 方法中的元件樹中。首先,找到建立 IconText 的程式碼,並將其刪除。在相同的位置,建立有狀態元件

dart
child: Row(
  children: [
    // ...
    Icon(
      Icons.star,
      color: Colors.red[500],
    ),
    const Text('41'),
    const FavoriteWidget(),
  ],
),

就是這樣!當你熱過載應用時,星星圖示現在應該可以響應點選了。

有問題嗎?

#

如果你的程式碼無法執行,請在你的 IDE 中查詢可能的錯誤。除錯 Flutter 應用 可能會有所幫助。如果你仍然找不到問題,請將你的程式碼與 GitHub 上的互動式湖泊示例進行比較。

如果你仍然有疑問,請參考開發者 社群 的任何一個渠道。


本頁的其餘部分涵蓋了元件狀態管理的幾種方式,並列出了其他可用的互動式元件。

狀態管理

#

誰管理有狀態元件的狀態?元件本身?父元件?兩者?另一個物件?答案是……這取決於。有幾種有效的方法可以使你的元件具有互動性。你作為元件設計者根據你期望元件的使用方式做出決定。以下是管理狀態的最常見方法

你如何決定使用哪種方法?以下原則應該可以幫助你做出決定

  • 如果相關狀態是使用者資料,例如複選框的選中或未選中模式,或者滑塊的位置,那麼最好由父元件管理該狀態。

  • 如果相關狀態是美觀的,例如動畫,那麼最好由元件本身管理該狀態。

如果你不確定,請從在父元件中管理狀態開始。

我們將透過建立三個簡單的示例:TapboxA、TapboxB 和 TapboxC 來演示不同的狀態管理方式。這些示例的工作方式類似——每個示例建立一個容器,當點選時會在綠色或灰色框之間切換。_active 布林值確定顏色:綠色表示活動狀態或灰色表示非活動狀態。

Active state Inactive state

這些示例使用 GestureDetector 來捕獲 Container 上的活動。

元件管理自己的狀態

#

有時,元件自己管理狀態是最有意義的。例如,ListView 會在內容超出渲染框時自動滾動。大多數使用 ListView 的開發人員不希望管理 ListView 的滾動行為,因此 ListView 本身管理其滾動偏移量。

_TapboxAState

  • 管理 TapboxA 的狀態。
  • 定義 _active 布林值,該值確定框的當前顏色。
  • 定義 _handleTap() 函式,該函式在點選框時更新 _active 並呼叫 setState() 函式來更新 UI。
  • 實現元件的所有互動行為。
dart
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,
        height: 200,
        decoration: BoxDecoration(
          color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
        child: Center(
          child: Text(
            _active ? 'Active' : 'Inactive',
            style: const TextStyle(fontSize: 32, 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()),
      ),
    );
  }
}

父元件管理元件的狀態

#

通常,讓父元件管理狀態並告訴其子元件何時更新是最有意義的。例如,IconButton 允許你將圖示視為可點選的按鈕。IconButton 是一個無狀態元件,因為我們決定父元件需要知道按鈕是否被點選,以便它可以採取適當的操作。

在下面的示例中,TapboxB 透過回撥將其狀態匯出到其父元件。由於 TapboxB 不管理任何狀態,因此它繼承自 StatelessWidget。

ParentWidgetState 類

  • 管理 TapboxB_active 狀態。
  • 實現 _handleTapboxChanged(),在點選框時呼叫的方法。
  • 當狀態改變時,呼叫 setState() 來更新 UI。

TapboxB 類

  • 繼承自 StatelessWidget,因為所有狀態都由其父元件處理。
  • 檢測到點選時,它會通知父元件。
dart
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,
        height: 200,
        decoration: BoxDecoration(
          color: active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
        child: Center(
          child: Text(
            active ? 'Active' : 'Inactive',
            style: const TextStyle(fontSize: 32, color: Colors.white),
          ),
        ),
      ),
    );
  }
}

混合搭配方法

#

對於某些元件,混合搭配方法最有意義。在這種情況下,有狀態元件管理部分狀態,而父元件管理狀態的其他方面。

TapboxC 示例中,按下時,方框周圍會出現深綠色的邊框。鬆開時,邊框消失,方框的顏色會改變。TapboxC 將其 _active 狀態匯出到其父元件,但內部管理其 _highlight 狀態。此示例有兩個 State 物件,分別是 _ParentWidgetState_TapboxCState

_ParentWidgetState 物件

  • 管理 _active 狀態。
  • 實現 _handleTapboxChanged(),在點選框時呼叫的方法。
  • 當發生點選事件且 _active 狀態發生變化時,呼叫 setState() 來更新 UI。

_TapboxCState 物件

  • 管理 _highlight 狀態。
  • GestureDetector 監聽所有點選事件。當用戶按下時,它會新增高亮(實現為深綠色邊框)。當用戶鬆開點選時,它會移除高亮。
  • 在按下、鬆開或取消點選時,如果 _highlight 狀態發生變化,則呼叫 setState() 來更新 UI。
  • 在點選事件發生時,將該狀態變化傳遞給父元件,以便使用 widget 屬性採取適當的操作。
dart
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,
        height: 200,
        decoration: BoxDecoration(
          color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
          border: _highlight
              ? Border.all(color: Colors.teal[700]!, width: 10)
              : null,
        ),
        child: Center(
          child: Text(
            widget.active ? 'Active' : 'Inactive',
            style: const TextStyle(fontSize: 32, color: Colors.white),
          ),
        ),
      ),
    );
  }
}

另一種實現方式可以將高亮狀態匯出到父元件,同時將活動狀態保留在內部,但如果你要求某人使用該點選框,他們可能會抱怨它沒有太大意義。開發者關心方框是否處於活動狀態。開發者可能不關心如何管理高亮,並且更喜歡點選框處理這些細節。


其他互動式元件

#

Flutter 提供了各種按鈕和類似的互動式元件。其中大多陣列件實現了 Material Design 指南,該指南定義了一組具有明確 UI 的元件。

如果你更喜歡,可以使用 GestureDetector 為任何自定義元件構建互動性。你可以在 管理狀態 中找到 GestureDetector 的示例。在 Flutter cookbook 的 處理點選 中瞭解更多關於 GestureDetector 的資訊。

當你需要互動性時,使用其中一個預製元件是最簡單的。

標準組件

#

Material 元件

#

資源

#

以下資源可能有助於你在應用中新增互動性。

手勢,Flutter cookbook 中的一部分。

處理手勢

如何建立一個按鈕並使其響應輸入。

Flutter 中的手勢

Flutter 手勢機制的描述。

Flutter API 文件

所有 Flutter 庫的參考文件。

精彩應用 正在執行的應用倉庫

具有自定義設計和引人入勝互動的 Flutter 演示應用。

Flutter 的分層設計 (影片)

此影片包含有關狀態和無狀態元件的資訊。由 Google 工程師 Ian Hickson 講解。