我們會用一個購物清單的實作,來帶大家認識元件在Flutter中的相互使用與堆疊。並且,在接下來的章節中,我們會將視野著重於程式構造,將不再細講單一特定元件的屬性與使用方式!
為了應對更加複雜的使用者互動,我們會使用「State(狀態)」來區分元件,最顯而易見的,就是使用Stateless與Stateful 元件。值得注意的是,我們先前介紹元件時大多都是使用「無狀態」的方式。
在StatefulWidgets中,會有setState()函數,當執行到該所屬函數時,會根據改變的狀態重新做UI的建構渲染
  void _increment() {
    setState(() { //使用setState,以告訴Flutter框架狀態已經改變,需要重新構建UI以反映變化
      _counter++;
    });
  }
我們拿一個使用ElevatedButton的計數器程式作為說明。
Counter類別: 是一個StatefulWidget。_CounterState類別: 是Counter的內部狀態,在這個類別內有_increment()函式_increment()函式: 裡面有setState()用於放置對變數_counter的變化行為ElevatedButton: 裡面的onPressed,會呼叫一次_increment()函式所以當程式執行時,會先建構初始化的計數器畫面,並隨著使用者點擊按鈕,呼叫_increment(),並由setState更新累計的次數,並顯示當前的次數在畫面上。
import 'package:flutter/material.dart';
class Counter extends StatefulWidget {
  const Counter({super.key});
  @override
  State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> { 
  int _counter = 0;
  void _increment() {
    setState(() { //我們使用setState,以告訴Flutter框架狀態已經改變,需要重新構建UI以反映變化
      _counter++;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        ElevatedButton(
          onPressed: _increment,//重新呼叫一次setState(),更新_counter
          child: const Text('Increment'),
        ),
        const SizedBox(width: 16),
        Text('Count: $_counter'),
      ],
    );
  }
}
void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: Center(
          child: Counter(),
        ),
      ),
    ),
  );
}
Stateful使用範例
我們回到購物車的範例,我們先設計出購物車項目的規劃,並且目前用inCart變數手動設為"false","true"來建構
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) {//設定購物車內的項目顏色
    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,//為false時是藍色、為true時是劃掉且顏色為灰色
            onCartChanged: (product, inCart) {},
          ),
        ),
      ),
    ),
  );
}
ShoppingListItem使用範例(Stateless,無狀態模式)
inCart true的情況

inCart false的情況

我們現在新增一個_handleCartChanged函數,使用setState()來控制每次的點擊互動更新,我們讓按鈕被點擊時固定呼叫此函數去更新inCart的Boolean值,點擊一次會使其「移除」、再次進行點擊則會「加回」
  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      if (!inCart) {
        _shoppingCart.add(product);
      } else {
        _shoppingCart.remove(product);
      }
    });
  }
_handleCartChanged函數
我們將剛剛Stateless的範例轉為使用Stateful來達成使用者動態更新購物車狀態,由剛剛的程式,我們可以新增_handleCartChanged函數處理購物車狀態,並設定三項購物車內容做demo
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) {
    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;
  @override
  State<ShoppingList> createState() => _ShoppingListState();
}
class _ShoppingListState extends State<ShoppingList> {
  final _shoppingCart = <Product>{};
  void _handleCartChanged(Product product, bool inCart) { //處理購物車狀態
    setState(() {
      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'),
      ],
    ),
  ));
}
ShoppingListItem使用範例(Stateful,有狀態模式)
