🚥

StreamSignalを使ってみた

2024/11/08に公開

📕Overview

Signals.dartのStreamの使用例について解説した記事を書いてみました。デモアプリではFirestoreからリアルタイムにデータを取得するのをやってみようと思います。

StreamSignal

Stream signals can be created by extension or method.

ストリーム・シグナルは、エクステンションまたはメソッドによって作成することができる。

https://dartsignals.dev/async/stream/

streamSignal

final stream = () async* {
    yield 1;
};
final s = streamSignal(() => stream);

toSignal()

final stream = () async* {
    yield 1;
};
final s = stream.toSignal();

.value, .peek()

Returns AsyncState<T> for the value and can handle the various states.

The value getter returns the value of the stream if it completed successfully.

値のAsyncState<T>を返し、様々な状態を扱うことができる。

値ゲッターは、正常に完了した場合にストリームの値を返します。

.peek() can also be used to not subscribe in an effect

final stream = (int value) async* {
    yield value;
};
final s = streamSignal(() => stream);
final value = s.value.value; // 1 or null

.reset()

The reset method resets the stream to its initial state to recall on the next evaluation.

リセット・メソッドは、ストリームを初期状態にリセットし、次の評価で呼び出す。

final stream = (int value) async* {
    yield value;
};
final s = streamSignal(() => stream);
s.reset();

.refresh()

Refresh the stream value by setting isLoading to true, but maintain the current state (AsyncData, AsyncLoading, AsyncError).

isLoadingをtrueに設定してストリーム値を更新しますが、現在の状態(AsyncData、AsyncLoading、AsyncError)は維持します。

final stream = (int value) async* {
    yield value;
};
final s = streamSignal(() => stream);
s.refresh();
print(s.value.isLoading); // true

.reload()

Reload the stream value by setting the state to AsyncLoading and pass in the value or error as data.

状態を AsyncLoading に設定してストリーム値を再読み込みし、値またはエラーをデータとして渡します。

final stream = (int value) async* {
    yield value;
};
final s = streamSignal(() => stream);
s.reload();
print(s.value is AsyncLoading); // true

Dependencies

By default the callback will be called once and the stream will be cached unless a signal is read in the callback.

デフォルトでは、コールバックは一度だけ呼び出され、コールバック内でシグナルが読み込まれない限り、ストリームはキャッシュされる。

final count = signal(0);
final s = streamSignal(() async* {
    final value = count();
    yield value;
});

await s.future; // 0
count.value = 1;
await s.future; // 1

If there are signals that need to be tracked across an async gap then use the dependencies when creating the streamSignal to reset every time any signal in the dependency array changes.

非同期のギャップを越えて追跡する必要があるシグナルがある場合は、streamSignalを作成するときに依存関係を使用して、依存関係の配列内のいずれかのシグナルが変更されるたびにリセットします。

final count = signal(0);
final s = streamSignal(
    () async* {
        final value = count();
        yield value;
    },
    dependencies: [count],
);
s.value; // state with count 0
count.value = 1; // resets the future
s.value; // state with count 1

🧷summary

StreamBuilderを使用した場合だと処理を多く書く必要がある。RiverpodのStreamProviderを使用すると簡潔に書けるが今回は、Signals.dartを使用した例なのでそちらをご紹介します。

StreamBuilderの場合

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

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

class _MyHomePageState extends State<MyHomePage> {
  final Stream<QuerySnapshot> _usersStream = FirebaseFirestore.instance.collection('users').snapshots();

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text('app'),
      ),
      body: StreamBuilder<QuerySnapshot>(
        stream: _usersStream,
        builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
          if (snapshot.hasError) {
            return Text('Something went wrong');
          }

          if (snapshot.connectionState == ConnectionState.waiting) {
            return Text("Loading");
          }

          return ListView(
            children: snapshot.data!.docs.map((DocumentSnapshot document) {
              Map<String, dynamic> data = document.data()! as Map<String, dynamic>;
              return ListTile(
                title: Text(data['name']),
              );
            }).toList(),
          );
        },
      ),
    );
  }
}

Signalsの場合

streamSignalを使用して、snapshotsをシグナルに変換する。状態をUI側で管理する。

// Firestoreのストリームをシグナルに変換
    final usersSignal = streamSignal(
            () => FirebaseFirestore.instance.collection('users').snapshots()
    );

Watchを使用して状態の変化を監視する。コードが短くなったという訳でもなかったですが、読みやすいコードになったのはではないかと思われます。flutter_hooksuseStream()と同じような感覚で使えると思われます。

body: Watch((context) {
        final state = usersSignal.value;

        // エラー処理
        if (state.hasError) {
          return const Text('Something went wrong');
        }

        // ローディング処理
        if (state.isLoading) {
          return const Text('Loading');
        }

        // データ取得成功時の処理
        final snapshot = state.value;
        if (snapshot == null) {
          return const Text('No data');
        }

        return ListView(
          children: snapshot.docs.map((document) {
            final data = document.data();
            return ListTile(
              title: Text(data['name']),
            );
          }).toList(),
        );
      }),

こちらが完成品のコードです。Riverpodよりは簡単かも?

全体のコード
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';

import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  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(),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    // Firestoreのストリームをシグナルに変換
    final usersSignal = streamSignal(
            () => FirebaseFirestore.instance.collection('users').snapshots()
    );

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('app'),
      ),
      body: Watch((context) {
        final state = usersSignal.value;

        // エラー処理
        if (state.hasError) {
          return const Text('Something went wrong');
        }

        // ローディング処理
        if (state.isLoading) {
          return const Text('Loading');
        }

        // データ取得成功時の処理
        final snapshot = state.value;
        if (snapshot == null) {
          return const Text('No data');
        }

        return ListView(
          children: snapshot.docs.map((document) {
            final data = document.data();
            return ListTile(
              title: Text(data['name']),
            );
          }).toList(),
        );
      }),
    );
  }
}

こんな感じでデータをリアルタイムに表示できます。

🧑‍🎓thoughts

今回は、Signals.dartを使用してCloud firestoreからリアルタイムにデータを表示するのをやってみました。日本ではRiverpod、海外ではBlocが流行っているようですが、Signalsも慣れるまでわからないが、短い処理、簡潔なコードでロジックを書いたり状態管理ができるようになるので、流行ってほしいなーと思いました。

可能性を感じるパッケージだと思っております。流行らせたい笑

Jboy王国メディア

Discussion