3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

導入

Flutterは、シンプルで美しいユーザーインターフェースを構築するための強力なフレームワークです。
しかし、複雑なアプリケーションを開発する際には、コードの整理や状態管理が課題となります。

今回は、riverpodriverpod_annotationを用いたMVVM (Model-View-ViewModel) パターンについて解説します。

MVVMパターンの概要

MVVMは、UIコードを整理し、コードの再利用性とテストの容易さを向上させるためのデザインパターンです。
以下の3つの主要コンポーネントから成り立っています。

  • Model: アプリケーションのビジネスロジックやデータを管理
  • View: ユーザーインターフェース (UI) のレンダリング
  • ViewModel: ModelとViewの間を仲介し、データの取得やUIへの反映を担当

コンポーネントの関係

+--------+         +-----------+         +-------+
|  View  | <-----> | ViewModel | <-----> | Model |
+--------+         +-----------+         +-------+

MVVMの特徴

  • 疎結合: ViewとModelは直接やり取りしません。ViewModelを通じて間接的にやり取りします。
  • テスト容易性: ビジネスロジックやデータロジックがViewModelやModelに集中しているため、UIをテストせずにロジックを単体テストできます。
  • 再利用性: ViewModelを再利用することで、異なるViewで同じロジックを利用できます。

Riverpodについて

FlutterのRiverpodは、状態管理と依存関係注入のためのパッケージです。Providerパッケージの進化形で、より柔軟で安全な設計になっています。
今回はflutter_riverpodriverpod_annotationの利用方法について触れていきます。

依存関係の追加

まず、pubspec.yamlに依存関係を追加します。
もしくは、pub addコマンドで、以下のpubspec.yamlに記載されたライブラリを追加します。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.5.1
  riverpod_annotation: ^2.3.5

dev_dependencies:
  build_runner: ^2.4.11
  riverpod_generator: ^2.4.2

各ライブラリをインストールする際には、最新のバージョンを以下から確認してください。

flutter_riverpod
riverpod_annotation
build_runner
riverpod_generator

コード生成の設定(任意)

build.yamlに設定を追加し、生成されるファイルの出力先や命名規則をカスタマイズできます。

build.yaml
targets:
  $default:
    builders:
      riverpod_generator:
        options:
          output_dir: lib/generated

flutter_riverpodの基本的な使い方

プロバイダーの定義

プロバイダーは、アプリ全体で共有したい値や状態を管理するためのものです。
以下の例では、カウンターの値を管理する StateProvider を定義しています。
MVVMにおけるViewModelに当たる部分になります。
※今回は基本的にModelは登場しません。

CounterProvider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

class Counter extends StateNotifier<int> {
  Counter() : super(0);

  void increment() {
    state++;
  }
  void decrement() {
    state--;
  }
}

final counterProvider = StateNotifierProvider.autoDispose<Counter, int>((ref) {
  return Counter();
});

ここで、autoDisposeというのはプロバイダーを適切に破棄してもらえるように設定するものになります。

プロバイダーの使用

ProviderScopeでラップすることによって、プロバイダーをアプリ全体で使用可能にします。

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

void main() {
  runApp(
    // [重要]
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: CounterScreen(),
    );
  }
}

Widgetでのプロバイダーの利用

上記のコード内に実装されているCounterScreenの中でcounterProviderを利用します。
プロバイダーの値を取得し、UIに反映させるには、StatelessWidgetではなく、ConsumerWidgetを使います。
また、StatefulWidgetに対応するConsumerStatefulWidgetというものも存在します。

このCounterScreenはMVVMのViewに当たる部分になります。

CounterScreen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class CounterScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // プロバイダーから値を取得
    final count = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('Counter'),
      ),
      body: Center(
        child: Text('Count: $count'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // プロバイダーの値を更新
          ref.read(counterProvider.notifier).increment();
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

以下のコードを利用することで、View(Widget)では、UIに関わる部分を、ViewModel(Provider/riverpod)では、値や状態の管理と、責任を分離することができます。

Riverpodのwatch/read/listen

ref.watchref.readref.listenは、Riverpodでプロバイダーの状態を操作したり、監視したりするためのメソッドです。
これらは頻繁に利用する重要なメソッドなので、それぞれの違いや具体的な使用例について解説します。

⚫︎ ref.watch

プロバイダーの状態をリアクティブに監視し、プロバイダーの状態が変化すると自動的にUIを再描画します。これは、状態をUIにバインドするために使用します。

上にあるCounterScreen.dartの例でも、buildメソッドの中ですぐに呼ばれています。

ref.watch(counterProvider)counterProviderの状態を監視し、countの値が変わるたびにUIを再描画します。

⚫︎ ref.read

ref.readは、プロバイダーの現在の状態を1回だけ取得します。
これは、状態を読み取って即時に処理を行いたい場合に使用し、リアクティブに監視しません。
ref.readは、基本的にボタンのコールバックなどで状態を変更したり、操作するために使用します。

上にあるCounterScreen.dartの例では、FloatingActionButtonのonPressedメソッドの中で呼ばれています。

ref.readConsumerWidgetConsumerStateのbuildメソッド内で使用すると、状態の変化に応じたUIの自動更新が行われず、期待通りに動作しません。
私はこの点を初め理解せず、readとwatchを混同して使っていましたが、注意が必要です。
UIが状態に応じて変化する場合や、プロバイダーの状態が変わるたびにUIを更新したい場合には、buildメソッド内でref.watchを使用しましょう。

⚫︎ ref.listen

ref.listenは、プロバイダーの状態の変化をトリガーして、状態が変化したときにコールバックを実行します。
以下に例を示します。

// プロバイダーの状態をリッスン
ref.listen<int>(
  counterProvider,
  (previousCount, newCount) {
    if (newCount > 5) {
      // countが5を超えたときにスナックバーによるアラートを表示
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('countが5を超えました!')),
      );
    }
  },
);

ref.listenの場合、状態が変わってもUIの再描画は行われませんが、上記の場合、countが5を超えると、スナックバーが表示される実装になっています。

リスナーが状態の変更を検知し、特定のロジックを実行する必要がある場合に使用します。

riverpod_annotation の基本的な使い方

次に、riverpod_annotationについて解説します。

プロバイダーの定義

flutter_riverpodと同様の状態管理用のカウンタープロバイダーを定義します。

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'counter_provider.g.dart';

@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;

  void increment() {
    state++;
  }
  void decrement() {
    state--;
  }
}

flutter_riverpodの場合に比べると、かなり少ない実装で済むのが特徴です。

コード生成

ターミナルでbuild_runnerを実行して、プロバイダーのコードを自動生成します。

flutter pub run build_runner build

これにより、.g.dartファイルが生成されます。

使用方法

使い方は、正直flutter_riverpodと同じであるので、割愛します。

参考(他のProvider)

StateNotifierProviderの他にも、以下のプロバイダーがあります:

  • FutureProvider: 非同期のデータ取得を管理します。
  • StreamProvider: ストリームのデータを管理します。

FutureProvider の例

定義

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'data_provider.g.dart';

@riverpod
Future<String> fetchData(FetchDataRef ref) async {
  // ダミーの非同期処理
  // 本来はAPIからデータを取得するなどの処理が記述されます。
  await Future.delayed(Duration(seconds: 2));
  return 'FetchedData';
}

ここで、FetchDataRefとは、自動生成されるリファレンス型です。

利用

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'data_provider.dart';  // 自動生成されたファイルをインポート

class DataAsyncScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final dataAsyncValue = ref.watch(fetchDataProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('Data'),
      ),
      body: Center(
        child: dataAsyncValue.when(
          data: (d) => Text('Data: $d'),
          loading: () => CircularProgressIndicator(),
          error: (e, _) => Text('Error: $e'),
        ),
      ),
    );
  }
}

whenメソッドの中で、非同期処理でデータ取得できたら、dataの部分の処理を、ロード中はloading、処理に失敗した場合はerrorの処理をそれぞれ実行するようになっています。

StreamProvider の例

中身としてはFutureProviderと大きな差はありません。

定義

import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'counter_stream_provider.g.dart';

@riverpod
Stream<int> counterStream(CounterStreamRef ref) async* {
  int count = 0;
  while (true) {
    await Future.delayed(Duration(seconds: 1));
    yield count++;
  }
}

利用

class CounterScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final counterStream = ref.watch(counterStreamProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('StreamProvider Example'),
      ),
      body: Center(
        child: counterStream.when(
          data: (count) => Text('Count: $count'),
          loading: () => CircularProgressIndicator(),
          error: (e, _) => Text('Error: $e'),
        ),
      ),
    );
  }
}

whenメソッドの部分も扱い方はFutureProviderと同様な形になります。

最後に

MVVMパターンは、UIのロジックを整理し、コードの再利用性とテストの容易さを向上させるデザインパターンです。
riverpod(flutter_riverpod)とriverpod_annotationを用いることで、プロバイダーの定義がシンプルになり、手動でコードを書く必要がなくなります。

これにより、コードのメンテナンス性と生産性が向上します。

是非これらのパッケージを利用し、かつMVVMを意識してもらえたらと思います!

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?