Introduction to Widgets

ジェスチャーの検知

  • ジェスチャーの検知はGestureDetectorで行う
  • 以下の感じで子要素に対してonTapの挙動を決めたりする
GestureDetector(
  onTap: () {
    print('MyButton was tapped!');
  },
  child: Container(
    height: 50.0,
    padding: const EdgeInsets.all(8.0),
    margin: const EdgeInsets.symmetric(horizontal: 8.0),
    decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(5.0),
      color: Colors.lightGreen[500],
    ),
    child: const Center(
      child: Text('Engage'),
    ),
  ),
);

入力でWidgetを変更する

  • StatefulなWidgetを利用すれば可能
  • 以下の形になる
class Counter extends StatefulWidget {
  ...constructor...

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extend State<Counter> {
  int _counter = 0 // State
  
  void _increment() { // Mutation
    setState(() { // これが必須
      _counter++;
    });
  }
  
  @override
  Widget build(BuildContext context) { // buildはState側に記述
    return ...
  }
  • 基本的に以下のように分かれている
    • Widget…コンストラクタとcreateState
    • State…State関連とbuild
  • このように分かれているのは2つのオブジェクトでライフサイクルが違うため。
    • Widget…一時的なオブジェクト
    • State…情報を永続的にもつ

直接データをbuildで使わない複雑な場合

  • 親WidgetにはCallbackで、子WidgetにはStateで状態が共有される
  • 以下の感じ
// CounterDisplay 受け取ったcounterを表示するのみ
class CounterDisplay extends StatelessWidget {
  const CounterDisplay({required this.count, super.key});

  final int count;

  @override
  Widget build(BuildContext context) {
    return Text('Count: $count');
  }
}

// CounterIncrementor onPressedというCallbackを受け取り、ボタンが押されたらそれを実行する
class CounterIncrementor extends StatelessWidget {
  const CounterIncrementor({required this.onPressed, super.key});

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      child: const Text('Increment'),
    );
  }
}

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

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      ++_counter;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        CounterIncrementor(onPressed: _increment), // ボタンタップ時に_increment()が実行され_counterが+1される
        const SizedBox(width: 16),
        CounterDisplay(count: _counter), // _counterの表示
      ],
    );
  }
}
  • こんな感じで状態を持つコンポーネント(Counter)と持たないで表示 or 変更処理をするコンポーネント(CounterDisplay, CounterIncrementor)に分ける

さらに複雑な例

  • ↑に加えてさらに複雑な例を記載します
class Product {
  const Product({required this.name});
  final String name;
}

typedef CartChangedCallback = Function(Product product, bool inCart);

// これはItemの1つの要素
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;
  
  // inCartフラグの値次第で、表示が変わる
  Color _getColor(BuildContext context) {
    // The theme depends on the BuildContext because different
    // parts of the tree can have different themes.
    // The BuildContext indicates where the build is
    // taking place and therefore which theme to use.

    return inCart //
        ? Colors.black54
        : Theme.of(context).primaryColor;
  }

  // inCartフラグの値次第で、表示が変わる
  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;

  // The framework calls createState the first time
  // a widget appears at a given location in the tree.
  // If the parent rebuilds and uses the same type of
  // widget (with the same key), the framework re-uses
  // the State object instead of creating a new State object.

  @override
  State<ShoppingList> createState() => _ShoppingListState();
}

class _ShoppingListState extends State<ShoppingList> {
  final _shoppingCart = <Product>{};
  
  // タップされたItemをCartに追加・削除する
  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      // When a user changes what's in the cart, you need
      // to change _shoppingCart inside a setState call to
      // trigger a rebuild.
      // The framework then calls build, below,
      // which updates the visual appearance of the app.

      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.0),
        children: widget.products.map((product) {
          return ShoppingListItem(
            product: product,
            inCart: _shoppingCart.contains(product), // inCartはcontainsの結果を渡している
            onCartChanged: _handleCartChanged,
          );
        }).toList(),
      ),
    );
  }
}