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

構建佈局教程向你展示瞭如何為以下截圖建立佈局。

The layout tutorial app
佈局教程應用

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

The custom widget you'll create

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

你可以直接在步驟 2:繼承 StatefulWidget中觸及程式碼。如果你想嘗試不同的狀態管理方式,請跳到管理狀態

有狀態和無狀態元件

#

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

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

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

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

建立一個有狀態元件

#

在本節中,你將建立一個自定義的有狀態元件。你將用一個自定義的有狀態元件替換兩個無狀態元件——實心紅色星星和它旁邊的數字計數——該元件管理一個帶有兩個子元件的行:一個 IconButtonText

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

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

本節將向你展示如何為 lakes 應用構建一個有狀態元件,名為 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,將管理自己的狀態。在這個例子中,切換星星是一個獨立的操作,不影響父元件或使用者介面的其他部分,因此元件可以在內部處理其狀態。

管理狀態中瞭解更多關於元件和狀態分離以及狀態如何管理的資訊。

步驟 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 上的互動式 lakes 示例進行對照檢查。

如果你仍然有問題,請參閱任何開發者社群頻道。


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

狀態管理

#

誰管理有狀態元件的狀態?元件本身?父元件?兩者兼有?還是另一個物件?答案是……視情況而定。有幾種有效的方法可以使你的元件具有互動性。作為元件設計者,你需要根據你期望元件如何使用來做出決定。以下是管理狀態最常見的方式

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

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

  • 如果所討論的狀態是美學方面的,例如動畫,那麼最好由元件本身管理狀態。

如有疑問,請從在父元件中管理狀態開始。

我們將透過建立三個簡單示例來展示管理狀態的不同方式: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 Components

#

資源

#

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

手勢,Flutter Cookbook 中的一個章節。

處理手勢
如何建立按鈕並使其響應輸入。
Flutter 中的手勢
Flutter 手勢機制的描述。
Flutter API 文件
所有 Flutter 庫的參考文件。
Wonderous 應用正在執行的應用倉庫
一個具有自定義設計和引人入勝互動的 Flutter 展示應用。
Flutter 的分層設計(影片)
此影片包含有關有狀態和無狀態元件的資訊。由 Google 工程師 Ian Hickson 主講。