パルカワ2

最近はFlutterをやっています

Riverpod を使ったアーキテクチャについて考える

前回この記事でRiverpodを使ったMVUアーキテクチャについて書いた。

hisaichi5518.hatenablog.jp

が、いくつか問題があるなと気づいた。

  • UIに関する処理を書く場所がない
  • 複数のStateを同時に扱う場合が考慮されていない(ビジネスロジックを書く場所がない)

なので、ViewModelを挟むのがよいのではと考えた。

// lib/screens/home/_home_view_model.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../providers/providers.dart';

final viewModelProvider = Provider((ref) => HomeViewModel(ref.read));

final navigationProvider = StateProvider((ref) => NavigationState.none);

/// 値を持たせたい場合は、freezedなどを使う
enum NavigationState {
  none,
  showSnackbar,
}

class HomeViewModel {
  HomeViewModel(this._read);

  final Reader _read;

  Future<void> increment() async {
    final countValue = _read(counterNotifierProvider);

    final count = countValue.data?.value;
    if (count == null) {
      return;
    }
    final newCount = count + 1;

    /// LocalStorageのデータも新しいデータで更新
    final sharedPreference = await _read(sharedPreferenceProvider.future);
    await sharedPreference.setInt('counter', newCount);

    /// counter以外、他のStateを更新しても問題ない

    /// 処理を行ったらStateの更新
    /// CounterNotifierのupdateを呼び出してStateを更新
    await _read(counterNotifierProvider.notifier).update(newCount);

    /// 更新後にSnackbarを表示する
    _read(navigationProvider).state = NavigationState.showSnackbar;
  }
}
// lib/screens/home/home_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../providers/providers.dart';
import '_home_view_model.dart';

class Home extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.listen<StateController<NavigationState>>(navigationProvider, (value) {
      switch (value.state) {
        case NavigationState.showSnackbar:
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('更新されました')),
          );
          break;
        default:
          break;
      }
    });

    return Scaffold(
      appBar: AppBar(title: const Text('Counter example')),
      body: Center(
        child: Consumer(builder: (context, ref, _) {
          /// 初期値の読み込みやStateが変更されたらrebuildなどwatchすれば行われる
          final count = ref.watch(counterNotifierProvider);

          /// 非同期でカウント数を読み込むので、loading, errorなどをちゃんと表現する
          return count.map(
            data: (data) => Text('${data.value}'),
            loading: (_) => const CircularProgressIndicator(),
            error: (_) => const Text('On Error'),
          );
        }),
      ),
      floatingActionButton: FloatingActionButton(
        /// ここでStateを更新するのではなくViewModelを経由する
        onPressed: () => ref.read(viewModelProvider).increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}

本来ViewModelは、UIに関する処理のみを担ってビジネスロジックは別のクラスであるUseCaseなどが担うべきな気はするけど、毎回UseCaseを作るのはめんどくさいので気にしていない。ビジネスロジックが大きくなってきたり共通化したい場合にUseCaseに分けるなどすればよいと思う。