🛰️

Signals.dart UI Stateを管理する

2024/08/23に公開1

👤対象者

  • Signalsに興味がある
  • 公式はカウンターしかないけどどう使うのか?
  • 入力機能で練習して理解する

offical
pub.dev

最近話題のSignalsに興味があって、使ってみた。そもそもこれどんなふうに使えばいいのか?
結論。setState()メソッドの代わりに、Widgetの状態管理に使います。

使い方

  1. StatefulWidgetのStateクラスの中に書く
  2. createSignal(context, 初期値)といった感じで定義する
  3. flutter_hooksのuseStateに似ている

varで書かないとエラー出る箇所もあるが、基本は、late finalで使う。初期値あるけど、lateいるんだな....

// <T>は、データ型。createSignal(context, T)は、(context, 初期値)です。
late final Signal<T> isDone = createSignal(context, T);

// このほうがわかりやすいか
late final Signal<データ型> isDone = createSignal(context, 初期値);

表示する部分や状態を変更する部分で、値を参照するには、.valueを使います。ここは、flutter_hooksと似ていますね。

Slider(
      value: slider.value,
      onChanged: (value) => slider.value = value,
      min: 0,
      max: 100,
      divisions: 100,
    ),

今回は、カウンター以外にもスライダー、Switch、drop downなどを作りました。

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

void main() {
  runApp(const App());
}

class App extends StatelessWidget {
  const App({super.key});

  ThemeData createTheme(BuildContext context, Brightness brightness) {
    return ThemeData(
      colorScheme: ColorScheme.fromSeed(
        seedColor: Colors.blue,
        brightness: brightness,
      ),
      brightness: brightness,
      useMaterial3: true,
    );
  }

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      themeMode: ThemeMode.system,
      home: const CounterExample(),
    );
  }
}

enum Status {
  TODO('未着手'),
  INPROGRESS('開始'),
  DONE('終了');

  final String description;

  const Status(this.description);
}

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

  
  State<CounterExample> createState() => _CounterExampleState();
}

class _CounterExampleState extends State<CounterExample> {
  // action chip
  late var favorite = createSignal(context, false);
  // switch light
  late final Signal<bool> light = createSignal(context, false);
  // checkbox
  late final Signal<bool> isDone = createSignal(context, false); 
  // drop down
  late final Signal<Status> status = createSignal(context, Status.TODO);
  // slider
  late final Signal<double> slider = createSignal(context, 0.0);
  // counter
  late final Signal<int> counter = createSignal(context, 0);
  // counter increment
  void _incrementCounter() {
    counter.value++;
  }
  // toggle
  void toggle() => isDone.value = !isDone.value;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Counter'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // switch light
            Switch(
              value: light.value,
              activeColor: Colors.amber,
              onChanged: (value) => light.value = value,
            ),
            // action chip
            ActionChip(
          avatar: Icon(favorite.value
          ? Icons.favorite : Icons.favorite_border),
          label: const Text('Save to favorites'),
          onPressed: () {
            favorite.value = !favorite.value;
          },
        ),
            // slider
            Text('Slider value: ${slider.value}'),
            Slider(
              value: slider.value,
              onChanged: (value) => slider.value = value,
              min: 0,
              max: 100,
              divisions: 100,
            ),
            // drop down
            DropdownButton<Status>(
              value: status.value,
              onChanged: (Status? value) {
                status.value = value!;
              },
              items: Status.values
                  .map((Status status) => DropdownMenuItem<Status>(
                        value: status,
                        child: Text(status.description),
                      ))
                  .toList(),
            ),
            const SizedBox(height: 16),
            // checkbox
            Checkbox(
              value: isDone.value,
              onChanged: (value) => toggle(),
            ),
            const SizedBox(height: 16),
            const Text(
              'You have pushed the button this many times:',
            ),
            // counter
            Text(
              '$counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

動作はこんな感じです

感想

使ってみた感想ですが、setState()メソッドと違って、ジャンクが出ているのが少ない気がしました。まだわからないですが🤔
使いこなせればパフォーマンスを上げることができるかもしれない???

aqさん、よこさんが使っているのをみて興味が出ました。普段は、flutter_hooks + hooks_riverpodですが、signals + flutter_riverpodを組み合わせてみたいとも思いました。

参考になりそうな記事たち

https://zenn.dev/mrken/articles/2e41ab0c1b3917
https://zenn.dev/zoome/articles/934a23c014c52b
https://zenn.dev/joo_hashi/articles/e543c5645bd725

Jboy王国メディア

Discussion

JboyHashimotoJboyHashimoto

createSignal deprecated

createSignalが非推奨になってしまった😱

'createSignal' is deprecated and shouldn't be used. use SignalsMixin > createSignal instead.
Try replacing the use of the deprecated member with the replacement.

代わりにSignalsMixin > createSignalを使用してください。
非推奨のメンバの使用を置き換えてみてください。

修正したコードを参考に今後は試してみてください🙏
まさか破壊的変更が出るとは...

StatefulWidgetのStateクラスにwithでSignalsMixinを多重継承する。

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

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with SignalsMixin {
  final _counter = signal(0);

  void _incrementCounter() {
    setState(() {
      _counter.value++;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Watch((context) {
              return Text(
                '$_counter',
                style: Theme.of(context).textTheme.headlineMedium,
              );
            }
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}