mcpField
S
signals-and-flutter-hooks

signals-and-flutter-hooks

github.com/rodydavis開発6056 回インストール2 週間前に更新
リポジトリ

signals-and-flutter-hooks について

Signals and Flutter Hooksは、setStateやValueNotifierからsignals、hooksまで、Flutterの状態管理をコード例で解説します。リアクティブな更新やウィジェットの再ビルド、ライフサイクル共有を学ぶ開発者向けです。

主な機能

  1. 01setStateとValueNotifierの比較
  2. 02signalsによるリアクティブな状態管理
  3. 03Watchによるウィジェット更新
  4. 04hooksによるStatefulWidgetの定型コード削減
  5. 05HookWidgetでのsignals利用

活用シーン

  1. 01Flutterの状態管理方式の比較
  2. 02状態更新をウィジェットの一部に限定
  3. 03HookWidgetでのリアクティブ状態共有

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rodydavis/skills --skill signals-and-flutter-hooks

インストール後、Claude Code で/signals-and-flutter-hooksを入力して読み込まれたか確認します。

詳細

Signals と Flutter Hooks

Flutter でデータを扱うとき、最初に触れることの一つが setState です。

setState

import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

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

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

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

  void increment() {
    if (mounted) {
      setState(() {
        count++;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(child: Text('Count: $count')),
      floatingActionButton: FloatingActionButton(
        onPressed: increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}

setState を呼び出すたびに、ウィジェットを単に更新対象としてマークしますが、更新がいつ発生するかを開発者が意識し、明示する必要があります。データを変更するときに setState の呼び出しを忘れると、ウィジェットツリーが古い状態のままになることがあります。

ValueNotifier

値を直接保存する代わりに ValueNotifier を使うと、これを改善できます。コンテナー内の値を読み書きでき、値の変更時にウィジェットツリーの一部を更新する ValueListenableBuilder などのヘルパーウィジェットを使えます。

import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

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

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

class _CounterState extends State<Counter> {
  final count = ValueNotifier(0);

  void increment() {
    count.value++;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(child: ValueListenableBuilder(
        valueListenable: count,
        builder: (context, value, child) {
          return Text('Count: $value');
        }
      )),
      floatingActionButton: FloatingActionButton(
        onPressed: increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}

FlutterSignal

signals パッケージを使うと、ValueNotifier を、プッシュ/プルアーキテクチャに基づくリアクティブグラフを使用する シグナルベースの実装 にアップグレードできます。

import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

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

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

class _CounterState extends State<Counter> {
  final count = signal(0);

  void increment() {
    count.value++;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: ValueListenableBuilder(
          valueListenable: count,
          builder: (context, value, child) {
            return Text('Count: $value');
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}

6.0.0 以降に作成された Signals は ValueNotifier も実装しているため、ほかのコードを変更せずに簡単に移行できます。

ValueListenableBuilder の代わりに、Watch ウィジェットまたは .watch(context) 拡張機能を使用できます。

import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

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

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

class _CounterState extends State<Counter> {
  final count = signal(0);

  void increment() {
    count.value++;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: Text('Count: ${count.watch(context)}'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}

flutter_hooks

Flutter Hooks を使うと、HookWidget に切り替えることで StatefulWidget のボイラープレートを減らせます。useState を使えば、build メソッド内で状態を直接定義し、ウィジェット間で簡単に共有できます。

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

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

  @override
  Widget build(BuildContext context) {
    final count = useState(0);
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(child: Text('Count: ${count.value}')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => count.value++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

useState は、変更時にウィジェットを自動的に再ビルドする ValueNotifier を返します

signals_hooks

新しいパッケージ signals_hooks を使うと、HookWidget 内でシグナルを定義できるようになり、ウィジェット間で共有可能なライフサイクルを備えたリアクティブグラフの利点を活用できます。

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:signals_hooks/signals_hooks.dart';

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

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

  @override
  Widget build(BuildContext context) {
    final count = useSignal(0);
    final countStr = useComputed(() => count.value.toString());
    useSignalEffect(() {
      print('count: $count');
    });
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(child: Text('Count: $countStr')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => count.value++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

コメント