跳到主內容

為您的 Flutter 應用新增互動性

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

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

構建佈局教程向您展示瞭如何建立以下截圖中的佈局。

The layout tutorial app

佈局教程應用

當應用首次啟動時,星形圖示是實心的紅色,表示該湖泊之前已被收藏。星形旁邊的數字表示有 41 個人收藏了這個湖泊。完成本教程後,點選星形圖示會取消其收藏狀態,將實心星形替換為空心輪廓,並減少計數。再次點選則會收藏該湖泊,顯示為實心星形並增加計數。

The custom widget you'll create

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

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

有狀態(Stateful)和無狀態(Stateless)元件

#

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

無狀態(Stateless)元件從不改變。IconIconButtonText 都是無狀態元件的示例。無狀態元件繼承自 StatelessWidget

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

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

建立有狀態元件

#

在本節中,您將建立一個自定義有狀態元件。您將用一個單一的自定義有狀態元件替換兩個無狀態元件(實心紅星和旁邊的數值),該元件管理著包含兩個子元件的行:一個 IconButton 和一個 Text

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

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

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

第 0 步:準備工作

#

如果您已經完成了構建佈局教程中的應用,請跳至下一節。

  1. 確保您已經設定好了您的開發環境。
  2. 建立一個新的 Flutter 應用.
  3. lib/main.dart 檔案替換為 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() 的函式引數在兩種狀態之間切換 UI

  • 一個 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 監聽所有點選事件。當用戶按下時,它新增高亮(實現為深綠色邊框)。當用戶釋放點選時,它移除高亮。
  • 在按下、鬆開或取消點選時呼叫 setState() 以更新 UI,並且 _highlight 狀態發生變化。
  • 在點選事件發生時,使用 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 食譜中的一個方案)中瞭解有關 GestureDetector 的更多資訊。

當您需要互動性時,最簡單的方法是使用預製元件之一。以下是部分列表

標準組件

#

Material 元件

#

資源

#

以下資源可能有助於您為應用新增互動性。

手勢,Flutter 食譜中的一個章節。

處理手勢

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

Flutter 中的手勢

Flutter 手勢機制的描述。

Flutter API 文件

所有 Flutter 庫的參考文件。

Wonderous 應用 執行應用倉庫

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

Flutter 的分層設計(影片)

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