Vue3のCompositionAPIライクにFlutter開発をしてみる話

## はじめに

この記事は[Webフロントエンジニアが、FlutterでもSignalを使ってアプリ開発してみたら最高だった話](https://zenn.dev/mrken/articles/2e41ab0c1b3917)の続きで、コード実装編になります。

実装に入る前の開発背景なども知りたい方は是非リンク元も見てみてください😇


## ディレクトリ構成

構成は以下のようにしました。

```
- constants (定数)
- models (モデル定義)
- routes (各ページ)
- services (モデルと1:1で結びついた処理、主に通信周り)
- states (状態管理) // ⭐️
- utils (状態伴わない汎用関数)
- widgets // ⭐️
    - common (共通Widget)
    - features (各画面ごとに必要なWidget)
        - home
        - setting
        ...
```

アプリ開発における一般的な構成だと思います。
widgetsはwebで言うcomponentsのことですが、個人開発でそんなにキッチリ分ける必要がなかったので**common/featureパターン**でざっくり分けています。

**そしてSignalはこの「widgets」と「states」にのみ存在します。**

## 具体的な実装の前にやりたいことの説明

説明していなかったのですが、
残念ながらFlutterにおける現状のSignalの認知度は**限りなく低いです。**

どれくらい低いかと言うと、「Flutter signal」で検索しても日本語記事が3つくらいしかありません。

そのどれもがAPIの使い方の説明であり、「じゃあどうやって実際に設計して使っていくの？」まで言及した記事はありません。

そのため私自身開発するにあたり、まずはSignalを使ったデータの受け渡し方を設計していくところから入りました。

幸いなことにSignalは特定の環境に依存しない、リアクティブプログラミング共通の概念でもあるので、
提供されているAPIやデータ更新/検知の流れはWebでもアプリでも変わりありません。（パッケージ自体Preactを参考に作られています）

そのためSignal系のフロント開発で一番実績があり、自身も慣れている
**Vue3のCompositionAPIをベースに考えるのは一つの最適解だと考えました。**

ちなみにVue.jsだと、公式ドキュメントにも記載のある以下が基本的な状態管理の書き方です。

```javascript
import { ref } from 'vue'

// モジュールスコープで生成されたグローバルな状態
const globalCount = ref(1) // ①

export function useCount() {
  // コンポーネント毎に生成されたローカルな状態
  const localCount = ref(1) // ②
  return {
    globalCount,
    localCount
  }
}
```

Vueでは**use〇〇**で始まる、これら状態を含んだ処理を**composable**と言い、他のcomposable内やcomponent内で使用できます。また状態はcomponent内でも定義できます。

```vue
<script setup>
const { globalCount, localCount } = useCount();
const count = ref(1) // ③
</script>
```

#### ここでのゴールはこれをDart言語で再現することです。

## 実装する

上で見て分かるように、Vue3のcompositionAPIでは状態のスコープは主に3つあります。

```
①グローバルステート
②ローカルステート (in composable)
③ローカルステート (in component)
```

①は分かりやすいですね。
アプリケーション全体を通して1つの状態をグローバルで共有するために使います。

②と③の違いは分かりづらいかもしれませんが、
③はそのコンポーネント内でのみ定義されて使われるのに対して、
②は他のコンポーネントやコンポーザブル内にも流用したい（ただしスコープは呼び出し先で閉じたい）場合に使われます。


まず分かりやすい①から実装しましょう！


```dart
// 定義元のコード
import 'package:signals/signals_flutter.dart';
final Signal<int> globalCount = signal(0); // グローバルな状態
```

**・・・はい、以上です。**
これだけでグローバルに存在する状態が作成できました。
以下のように使用します。

```dart
// 呼び出し先のコード
import '/src/states/count.dart';
print(globalCount.value);
```

え・・これだけで動くの？って感じですが、はい、これでグローバルに状態を共有した状態になります。
（語弊はありますが）riverpodの役目をたった一行で果たしてくれる訳ですね。

-----

では次に②を考えてみましょう。
Vueだと関数の中に状態を入れるのですが、Dartではそれはしません。
何故なら**Dartは分割代入が出来ないからです😭**

つまり以下のような書き方はサポートされていません。
```dart
final { globalCount, localCount } = useCount();
```

出来るとしても```state['localCount']```みたいな感じです。
これではコード補完も効きませんし、ただただキツいの一言。

色々と探した結果、**パターンマッチング**という手法があることを知りました。
クラスのパラメータとして返すことで、呼び出し先で擬似的に分割代入ぽい書き方ができるようになります。

具体的には以下のような形です。


```dart
// 定義元のコード
import 'package:signals/signals_flutter.dart';

// カウントクラス
class Count {
  final Signal<int> localCount = signal(0); // ローカルな状態
}
```

```dart
// 呼び出し先のコード
import '/src/states/count.dart';
final Count(:localCount) = Count();
```

これで```localCount```は補完も型も効くようになりますし、
ここで呼び出された```localCount```のスコープはその呼び出された場所で閉じられるようになります。

さて①と②の動きまでは確認できたのですが、
どこからでも呼ばれることを考えるとグローバル汚染には気をつけたいですし、
①と②の呼び出し方も統一したいですね。


以下のように修正しましょう。

```dart
import 'package:signals/signals_flutter.dart';

final Signal<int> _globalCount = signal(0);

// カウント管理クラス
class Count {
  Signal<int> get globalCount => _globalCount; // グローバルな状態
  final Signal<int> localCount = signal(0); // ローカルな状態
  Count._internal();
}

Count useCount() {
  return Count._internal();
}
```

はい、上記が私が思う**最善の基本形**です。
まず```globalCount```の方は変数定義に_(アンダースコア)を付けます。
**Dartの言語仕様で_を付けると、そのファイル外からのアクセスができない、つまりprivateになります。**

その上でCountクラスに```_globalCount```を返すゲッター```globalCount```を定義します。
これで```_globalCount```は、**実質グローバルではあるけどCountクラスのフィールドになりパターンマッチングで拾えるようになります。**

もし```_globalCount```を完全にそのファイル内だけでしか操作させたくない場合は、以下のように```ReadOnlySignal```にして```addGlobalCount()```のような関数をクラス内部で書けば良いですし、
```dart
ReadOnlySignal<int> get globalCount => _globalCount;
addGlobalCount() {
  _globalCount.value++;
}
```

加工して返したければ以下のように```ComputedSignal```を使うなど、かなり自由度が高く書けます。
```dart
ComputedSignal<int> get doubleGlobalCount => computed(() => _globalCount.value * 2);
```

ちなみにクラス内に書かれた最後の```Count._internal();```は、クラス外部からのインスタンス化を制限するために使われます。

このようにすることで、Countクラスのインスタンスを直接作成できなくなり、インスタンスの作成方法をクラス内部に制限することができます。

**つまり唯一```useCount（）```関数を通してのみ、この状態クラスを扱えるようになります。**

use〇〇という関数名を必ずしも命名しないといけないわけではないのですが、
慣習的に「**あ、状態を持っているんだな**」と差別化できて分かりやすいのでそうしています。

また設計時Singletonパターンもどうかと考えましたが、
それだとグローバルな状態とローカルな状態の両立は難しく、またVueのcompositionAPIの動きからもかけ離れているのでペケです。

ではこの状態クラスを実際のwidgetで使っていきましょう。
vsCodeで「stls」と書けば、以下のようなStatelessWidgetのテンプレがまず作れると思います。

```dart
class MyWidget extends StatelessWidget {
  const MyWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}
```

今回用にちょっと整えます。

```dart
class CountWidget extends StatelessWidget {
  CountWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
        　　　　children: [
          　　　　Text('GlobalCount is '),
          　　　　Text('LocalCount is '),
        　　　　],
      　　　　),
        ElevatedButton(
          onPressed: () {},
          child: Text('カウントアップ'),
        )
      ],
    );
  }
}
```

そして肝心の状態を読み込みます。

```dart
class CountWidget extends StatelessWidget {
  final count = useCount(); // ①
  CountWidget({super.key});

  @override
  Widget build(BuildContext context) {
    final Count(:globalCount, :localCount) = count; // ②
    return Column(
      children: [
        Watch( // ③
          (context) => Row(
            children: [
              Text('GlobalCount is ${globalCount.value}'),
              Text('LocalCount is ${globalCount.value}'),
            ],
          ),
        ),
        ElevatedButton(
          onPressed: () {
            localCount.value++;
            globalCount.value++;
          },
          child: Text('カウントアップ'),
        )
      ],
    );
  }
}
```


#### どうですか？ Signal系フレームワークを触ったことのあるフロントエンジニアならかなり直感的に分かる内容ではないでしょうか？


①まずWidget内で状態クラスをインスタンス化します。
②buildメソッド内で必要な変数、関数をパターンマッチングで拾ってきます。
③テンプレート内でSignalを使いたい箇所をWatchで囲います。

これでボタンを押した時、カウントアップされている様子が確認できます。
また状態を保持しているのはWidget自身ではなく状態管理クラスになるので、この場合は```StatelessWidget```を使うべきと私は判断しています。

後ほど```StatefulWidget```での使用例を出しますが、
```setState()```との大きな違いは、**再レンダリングの対象がbuildメソッド全体ではなく```Watch()```内に限定されるということ**です。

例えばボタンの文章を```Text('カウントアップ${globalCount.value}')```などとしても、Watch外なので変化が起きません。

**今回のアプリ開発でsetState()によるbuild単位での更新は一度も使用する機会がありませんでした。**

ちなみにVueだと```Watch```すら書かずに、テンプレート内は**必要な要素だけ自動で再レンダリング**してくれます。
Vueがパフォーマンス面で優れているのもそういったことが理由です。

------

では③状態をWidget内で定義する場合はどうでしょうか？
この場合は状態はWidgetが持っていると見做されるので```StatefulWidget```を使います。

先ほどと同じようにvsCodeで「stfl」と打つとStatefulWidgetのテンプレが出てきます。

```dart
class MyWidget extends StatefulWidget {
  const MyWidget({super.key});

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}
```

今回用に整えます。

```dart
class CountWidget extends StatefulWidget {
  const CountWidget({super.key});

  @override
  State<CountWidget> createState() => _CountWidgetState();
}

class _CountWidgetState extends State<CountWidget> {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Watch(
          (context) => Row(
            children: [
              Text('GlobalCount is '),
              Text('LocalCount is '),
              Text('WidgetCount is '),
            ],
          ),
        ),
        ElevatedButton(
          onPressed: () {},
          child: Text('カウントアップ'),
        )
      ],
    );
  }
}
```

状態を作成します。

```dart
class CountWidget extends StatefulWidget {
  const CountWidget({super.key});

  @override
  State<CountWidget> createState() => _CountWidgetState();
}

class _CountWidgetState extends State<CountWidget> with SignalsAutoDisposeMixin { // ①
  final count = useCount();
  late final widgetCount = createSignal(context, 0); // ②

  @override
  Widget build(BuildContext context) {
    final Count(:globalCount, :localCount) = count;
    return Column(
      children: [
        Watch(
          (context) => Row(
            children: [
              Text('GlobalCount is ${globalCount.value}'),
              Text('LocalCount is ${localCount.value}'),
              Text('WidgetCount is ${widgetCount.value}'),
            ],
          ),
        ),
        ElevatedButton(
          onPressed: () {
            localCount.value++;
            globalCount.value++;
            widgetCount.value++;
          },
          child: Text('カウントアップ'),
        )
      ],
    );
  }
}
```

はい、先ほどとの違いは```SignalsAutoDisposeMixin```を使っていることと、状態を```createSignal```で作成していることです。

```SignalsAutoDisposeMixin```は無くても機能するのですが、
指定しておくとWidgetの破棄のタイミングでWidget内で定義されたSignalが自動でdisposed()してくれます。

ちなみにクラス内で一度```final count = useCount();```でインスタンス化せずに、
いきなりbuildメソッド内で```final Count(:globalCount, :localCount) = useCount();```とした方がWebの書き方に近いのですが、
buildメソッドは色んなタイミングで発生するので無駄にインスタンス化される可能性があるのと、
作成した状態クラスは```initState()```などbuildメソッド以外でも使われるので、初期化は一度だけ、あとはパターンマッチングで適宜拾うという形がパフォーマンス的にも良いと思われます。

とりあえずこれでSignalを使った状態クラスの作り方と、Widget内での使い方の説明は以上です。
次にModel層やService層を考慮した実践例を出します。

## チャットに必要な設計を考えてみる

アプリ開発時のコードをかなり端折って解説します。（実際の処理内容とは異なります）
まず```models/chat.dart```を用意して以下のようにモデル定義を行います。

```dart
import 'package:cloud_firestore/cloud_firestore.dart';

class ChatData {
  final String uid;
  final String name;
  final String message;
  final DateTime createdAt;
  final DateTime updatedAt;

  ChatData({
    required this.uid,
    required this.name,
    required this.message,
    required this.createdAt,
    required this.updatedAt,
  });

  factory ChatData.fromFirestore(DocumentSnapshot doc) {
    Map<String, dynamic> data = doc.data() as Map<String, dynamic>;

    return ChatData(
      uid: data['uid'],
      name: data['name'],
      message: data['message'],
      createdAt: (data['createdAt'] as Timestamp).toDate(),
      updatedAt: (data['updatedAt'] as Timestamp).toDate(),
    );
  }

  Map<String, dynamic> toFirestore() {
    return {
      'uid': uid,
      'name': name,
      'message': message,
      'createdAt': FieldValue.serverTimestamp(),
      'updatedAt': FieldValue.serverTimestamp(),
    };
  }

  @override
  String toString() {
    return 'ChatData(uid: $uid, name: $name, message: $message,　createdAt: $createdAt), updatedAt: $updatedAt)';
  }
}

```

特に解説することはありませんが、firebaseに入れるデータ構造の定義と、それを入れたり出したりする際の変換用関数（```fromFirestore、toFirestore```）をここでは定義しました。
次に```services/chat.dart```を用意して、このモデルを扱うためのコードを書いていきます。

```dart
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';

class ChatService {
  // 参照先
  final chatCollection = FirebaseFirestore.instance.collection('chats');

  // チャット情報を取得
  Future<List<ChatData>> fetchChatList({
    int? num,
  }) async {
    try {
      // 初期化
      num ??= 10;

      final querySnapshot = await chatCollection.orderBy('createdAt', descending: true).limit(num).get();

      // チャット情報が存在しなければ空配列で返す
      if (querySnapshot.docs.isEmpty) return [];

      // チャット型に整形して返す
      return querySnapshot.docs.map((log) => ChatData.fromFirestore(log)).toList();
    } catch (onError) {
      Log.e('Error: $onError');
      return [];
    }
  }

  // チャット情報を登録
  Future<bool> postChat({
    required String uid,
    required String name,
    required String message,
  }) async {
    try {
      // チャット情報
      final chatData = ChatData(
        uid: uid,
        name: name,
        message: message,
        createdAt: DateTime.now(),
        updatedAt: DateTime.now(),
      ).toFirestore();

      final chatRef = chatCollection.doc();
      await chatRef.set(chatData);

      return true;
    } catch (onError) {
      Log.e('Error: $onError');
      return false;
    }
  }
```

ここではチャットコレクションへアクセスして情報を取ってきたり、データ挿入のためのコードを書いています。

**重要なのは各Serviceクラスは対象となるモデルにのみ関心を持ち、service間で依存し合うような実装や、それより上位層となるstatesの読み込みは避けるべきということです**

理由はそうすることで、依存関係を考えずにシンプルに実装できるからです。
そしてこれらは、これから解説する**states**などの上位層で使用します。

それでは```states/chatManager.dart```を用意して、いよいよ状態管理を行うためのコードを見ていきましょう。

```dart
import 'dart:async';
import 'package:signals/signals_flutter.dart';
import 'src/models/chat.dart';
import 'src/services/chat.dart';
import 'src/states/application_setting.dart';
import 'src/states/error_manager.dart';
import 'src/states/system_manager.dart';
import 'src/states/user_manager.dart';
import 'src/utils/log.dart';

final _applicationSetting = useApplicationSetting();
final _userManager = useUserManager();
final _errorManager = useErrorManager();
final _systemManager = useSystemManager();

// グローバルState
final Signal<List<ChatData>> _chatList = signal([]);

// チャットの状態管理クラス
class ChatManager {
  // チャット一覧
  Computed<List<ChatData>> get chatList => computed(() {
        return [..._chatList.value]..sort((a, b) => b.createdAt.compareTo(a.createdAt));
      });

  // チャットを取得
  Future<void> getChatList({
    required int num, // 取得数
    required bool isOverride, // 上書きかどうか
  }) async {
    try {
      final ApplicationSetting(:blockList) = _applicationSetting;
      final UserManager(:user, :getUser) = _userManager;
      final SystemManager(:isLocalMode, :isMaintenance) = _systemManager;

      // 通信できない状態ならreturn
      if (isLocalMode.value || isMaintenance.value) return;
      // ユーザ情報取得前であれば先に取得する
      if (user.value == null) await getUser();

      var result = await ChatService().fetchChatList(num: num);
      // ブロック中のユーザの書き込みであれば除外
      result = result.where((chat) => !blockList.value.contains(chat.uid)).toList();

      _chatList.value = isOverride ? result : [..._chatList.value, ...result];
    } catch (onError) {
      Log.e('Error: $onError');
    }
  }

  // チャットを送信
  Future<bool> sendChat({required String message}) async {
    try {
      final ApplicationSetting(:nickname) = _applicationSetting;
      final UserManager(:user, :getUser) = _userManager;
      final SystemManager(:isLocalMode, :isMaintenance) = _systemManager;

      // 通信できない状態ならreturn
      if (isLocalMode.value || isMaintenance.value) return false;
      // ユーザ情報取得前であれば先に取得する
      if (user.value == null) await getUser();

      // チャット送信
      final isSuccess = await ChatService().postChat(
　　　　　　　　　　　　　　　　uid: user.value.uid,
        name: '${nickname.value}',
        message: message,
      );
      if (!isSuccess) throw Exception('チャットの送信に失敗しました。');
      await getChatList(num: 10, isOverride: true); // チャット再取得

      return true;
    } catch (onError) {
      final ErrorManager(:showErrorDialog) = _errorManager;
      showErrorDialog(onError.toString());
      Log.e('Error: $onError');

      return false;
    }
  }

  ChatManager._internal();
}

ChatManager useChatManager() {
  return ChatManager._internal();
}
```

はい、ちょっとごちゃごちゃしていますが、状態クラスの基本形は先ほど説明したのと同じ構成です。

違いがあるとするならば、**実際は1つの状態管理クラスの中で、別の様々な状態管理クラスを呼んでいるということです。**

例えば```useApplicationSetting()```では、
sharedPreferences(Webで言うlocalStorage)に保管されている内容をSignalとして取り出せるようにしているため、そこからブロックしているユーザ情報を引っ張ってきてチャットをフィルタしたり、```useSystemManager()```ではメンテナンス状態や端末の通信状態を保管しているので、それによって通信しないようにしたり、といった制御を加えています。

これはVue3開発でも同様で、色んなところで定義した状態を必要に応じて摘んで組み立てていくことから、**compositionAPI**と命名されているのだと思います。


ここでSignalの挙動に関しての補足ですが、
Signalは更新があると、そのSignalに依存している他の対象が自動的に再評価されます。
ただし再評価が行われるのは、実際に値に更新があった時ではなく正確には**それが読み込まれた時**です。

例えば先ほどの例のようにbuildメソッドの**Watch内**で、Signalの値をUIで使用していれば、これは常に読み込んでいると言えるので**値が更新されればそのままUIが切り替わります。**

一方でbuildメソッドの**Watch外**でSignalの値を使用していても、
実際に値の更新が検知されるのは**次にbuildメソッドが走ったタイミングになります。**

このように様々なstateが複雑に絡み合った状態にあっても、
それが現在のコンテキストにおいて監視されていない限りは無駄に呼び出され、再計算コストが掛かるわけではないので安心して使えます。

ただし1つの状態管理クラスに何でもかんでも入れると確実に管理が大変になるので、
**状態管理クラスは極力その責務を明確に分けるなど、設計力はある意味問われるところだと思います😎**

さてここまでで一通りの解説を終えました。
最後に少しばかりTipsを紹介していきます。

## Tips

### Signalの値が変わるたびに勝手にログが出るのが鬱陶しい

デフォルトでprintされるようになっています、main.dartで以下のように書いておきましょう。
```
// Signalの自動追跡をオフに
SignalsObserver.instance = null;
```

ちなみにパフォーマンスチューニング時にまたコメントアウトを解除して、無駄な計算が発生していないか見るのがおすすめです。

今回のアプリ開発でタイマー機能もSignalで管理していたのですが、1秒おきに無駄にComputedが走っていたりなど、このログのおかげでいくつか改善できました。


### sharedPreferencesとSignalを連携したい

まず以下の2つのファイルを作成します。

[states/shared_preferences.dart]
```dart
import 'package:shared_preferences/shared_preferences.dart';

// sharedPreference用キー情報
class SharedPreferencesKey {
  static const blockList = 'blockList';
}

// SharedPreference用のシングルトンクラス
class SharedPreference {
  static final SharedPreference _instance = SharedPreference._internal();
  SharedPreferences? _prefs;

  factory SharedPreference() {
    return _instance;
  }

  SharedPreference._internal();

  Future<void> initPrefs() async {
    _prefs ??= await SharedPreferences.getInstance();
  }

  SharedPreferences? get prefs => _prefs;
}

SharedPreference useSharedPreference() {
  return SharedPreference();
}
```

[states/application_setting.dart]
```dart
import 'package:signals/signals_flutter.dart';
import 'package:until_moss_grows/src/states/shared_preference.dart';

// SharedPreferencesのインスタンス
final _sharedPreference = useSharedPreference();

// ブロック設定
final Signal<List<String>> _blockList = signal([])
  ..subscribe((value) {
    _sharedPreference.prefs?.setStringList(SharedPreferencesKey.blockList, value);
  });

// 設定用クラス
class ApplicationSetting {
  // ブロックリスト
  Signal<List<String>> get blockList => _blockList;

  // 初期化
  void initApplicationData() async {
    if (_sharedPreference.prefs == null) return;
    _blockList.value = _sharedPreference.prefs!.getStringList(SharedPreferencesKey.blockList) ?? [];
  }

  ApplicationSetting._internal();
}

ApplicationSetting useApplicationSetting() {
  return ApplicationSetting._internal();
}
```

SharedPreferencesへアクセスするためのインスタンス自体はSingletonで扱われている記事が多かったのでそうしています。
shared_preferences.dartは一度作成したら、SharedPreferencesKeyに追加していくこと以外更新することはありません。

初期化方法はmain.dart内で```runApp(MyApp())```となっていたら以下のような感じです。

```dart
class MyApp extends StatefulWidget {
  const UntilMossGrows({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final sharedPreference = useSharedPreference();
  final applicationSetting = useApplicationSetting();

  @override
  void initState() {
    super.initState();

    final SharedPreference(:initPrefs) = sharedPreference;
    final ApplicationSetting(:initApplicationData) = applicationSetting;

    // 初期化
    initPrefs().then((value) {
      initApplicationData();
    });
  }
```

これで起動時、sharedPreferecesに保管された情報を状態として読み込んでいるので、以降はどのファイルにいても、useApplicationSetting()から取得できるようになります。

また```..subscribe()```により、状態を更新したらそのまま自動でsharedPreferencesへ保管されるようになります。
これはVueUseで言う```useLocalStorage()```みたいな感じです。

### デバイスサイズをSignalで管理したい

個人的にはかなり必須だと思うので以下をそのままご使用ください。

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

// グローバルState
final Signal<double> _width = signal(0);
final Signal<double> _height = signal(0);
final Signal<double> _keybordHeight = signal(0);
final Signal<EdgeInsets> _safePadding = signal(EdgeInsets.zero);

// デバイスサイズ監視クラス
class DeviceSize {
  // 横幅
  ReadonlySignal<double> get width => _width;

  // 縦幅
  ReadonlySignal<double> get height => _height;

  // キーボード高さ
  ReadonlySignal<double> get keybordHeight => _keybordHeight;

  // 余白込みの幅取得
  double withSafeSize({double? top, double? left, double? right, double? bottom}) {
    if (top != null) return top + _safePadding.value.top;
    if (left != null) return left + _safePadding.value.left;
    if (right != null) return right + _safePadding.value.right;
    if (bottom != null) return bottom + _safePadding.value.bottom;
    return 0;
  }

  // 初期化
  void setDeviceSize(BuildContext context) {
    _width.value = MediaQuery.of(context).size.width;
    _height.value = MediaQuery.of(context).size.height;
    _safePadding.value = MediaQuery.of(context).padding;
    _keybordHeight.value = MediaQuery.of(context).viewInsets.bottom;
  }

  DeviceSize._internal();
}

DeviceSize useDeviceSize() {
  return DeviceSize._internal();
}
```

初期化方法は先ほどのmain.dartのbuildメソッド内に以下を追加するだけです。

```dart
  @override
  Widget build(BuildContext context) {
    final DeviceSize(:setDeviceSize) = deviceSize;
    setDeviceSize(context);

    return MaterialApp(...
```

あとはどこからでも、例えそれがbuildCountext外であっても、
```final DeviceSize(:width, :withSafeSize) = useDeviceSize();```のように呼び出せます。**以上!**

## 最後に

いかがでしたか？
私自身アプリ開発の経験はこれが初めてだったので[アプリ](https://apps.apple.com/app/id6547858108)のリリースまでは時間が掛かったのですが、
Signalのお陰でCompositionAPIをDartで扱っているような感覚で終始開発が出来ていました😇

#### ぜひ皆さんもSignalを使ってFlutterアプリ開発をしてみてください！以上です！