目次
概要
開発コストを低くするためにも、そして複数名のエンジニアとチームを組んで開発する上でもアーキテクチャを意識した開発は大切だ。求められるエンジニアになるためにも。
アーキテクチャは様々だが、Flutterではriverpodが使われる。これはFlutterの「状態管理ライブラリ」だ。アーキテクチャを意識した記事はほぼ見かけなかったため記録に残す。
今回はjsonファイルを使ってデータのやり取りを行う。
準備
riverpodのインストール
riverpodはライブラリなのでインストールする必要がある。
まずはターミナルを開こう。AndroidStudioを使っているなら、下の方にあるこのターミナルアイコンを選択しよう。

そこで、以下のコマンドを叩く。
$ flutter pub add flutter_riverpodそして、pubspec.yamlファイルにが追加されていることを確認する
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8
  flutter_riverpod: ^2.6.1そうするとflutter_riverpodをimportできるようになる。
import 'package:flutter_riverpod/flutter_riverpod.dart';jsonファイルを追加
今回はデータの取得して表示するという処理をjsonファイルを用いて行う。
詳しくはこちらに記載する。
ファイル構成
今回はホーム画面(HomeView)のボタンを押下したら住所のデータをjsonファイルから取得して表示するということをやってみる。
ファイル構成は以下のようになっている。
- images(画像ファイルを格納する)
- json(jsonファイルを格納する)
- lib(Flutterのファイル群)
- Entity(レスポンスデータクラスなどを格納する)
 - Repository
- Service
- View
- ViewModel
 
図で表すと以下のようなイメージだ。

- Repository
- Service
 API通信やデータベースへの参照を行う。
 同時に、Repositoryへ取得したデータを渡す処理を行う。
- View
 UIなど画面表示を行うファイル群を保存しておく。
- ViewModel
 ViewとRepositoryとの橋渡しを行う。
ソースコード
JSONファイル
{
  "message": null,
  "results": [
    {
      "address1": "北海道",
      "address2": "美唄市",
      "address3": "上美唄町協和",
      "kana1": "ホッカイドウ",
      "kana2": "ビバイシ",
      "kana3": "カミビバイチョウキョウワ",
      "prefcode": "1",
      "zipcode": "0790177"
    },
    {
      "address1": "北海道",
      "address2": "美唄市",
      "address3": "上美唄町南",
      "kana1": "ホッカイドウ",
      "kana2": "ビバイシ",
      "kana3": "カミビバイチョウミナミ",
      "prefcode": "1",
      "zipcode": "0790177"
    }
  ],
  "status": 200
}
レスポンスクラス
class ZipCloudEntity {
  final String address1;
  final String address2;
  final String address3;
  final String kana1;
  final String kana2;
  final String kana3;
  final String prefCode;
  final String zipcode;
  ZipCloudEntity({
    required this.address1,
    required this.address2,
    required this.address3,
    required this.kana1,
    required this.kana2,
    required this.kana3,
    required this.prefCode,
    required this.zipcode,
  });
  factory ZipCloudEntity.fromData(dynamic data) {
    final String address1 = data['address1'];
    final String address2 = data['address2'];
    final String address3 = data['address3'];
    final String kana1 = data['kana1'];
    final String kana2 = data['kana2'];
    final String kana3 = data['kana3'];
    final String prefCode = data['prefcode'];
    final String zipcode = data['zipcode'];
    final model = ZipCloudEntity(
      address1: address1,
      address2: address2,
      address3: address3,
      kana1: kana1,
      kana2: kana2,
      kana3: kana3,
      prefCode: prefCode,
      zipcode: zipcode,
    );
    return model;
  }
}
View
メイン部分
import 'package:flutter/material.dart';
import 'package:flutter_demo_application/View/home_view.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
  runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Demo Application',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: HomeView(),
    );
  }
}
ホーム画面
import 'package:flutter/material.dart';
import 'package:flutter_demo_application/Entity/zip_cloud_entity.dart';
import 'package:flutter_demo_application/view_model/home_view_model.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class HomeView extends ConsumerWidget {
  HomeView({super.key});
  List<ZipCloudEntity> zipCloudList = <ZipCloudEntity>[];
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    var button = OutlinedButton(
      onPressed: () {
        Future<List<ZipCloudEntity>> zipCloudStateProvider =
            ref
                .watch(homeViewModelStateNotifierProvider.notifier)
                .getStateZipCloudData();
        zipCloudStateProvider.then((list) {
          zipCloudList = list;
          ref.invalidate(homeViewModelNotifierProvider);
        });
      },
      style: OutlinedButton.styleFrom(
        backgroundColor: Colors.white10,
        foregroundColor: Colors.black,
        disabledBackgroundColor: Colors.black26,
        disabledForegroundColor: Colors.black54,
      ),
      child: Text("データ取得"),
    );
    return MaterialApp(
      theme: ThemeData(primarySwatch: Colors.grey),
      home: Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: const Text(''),
        ),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            button,
            ref
                .watch(homeViewModelNotifierProvider)
                .when(
                  data:
                      (_) => ListView.builder(
                        scrollDirection: Axis.vertical,
                        shrinkWrap: true,
                        itemCount: zipCloudList.length,
                        itemBuilder: (_, index) {
                          final zipCloud = zipCloudList[index];
                          return zipCloudCell(zipCloud);
                        },
                      ),
                  error: (error, _) => const Center(child: Text('通信エラー')),
                  loading:
                      () => const Center(child: CircularProgressIndicator()),
                ),
          ],
        ),
      ),
    );
  }
  Widget zipCloudCell(ZipCloudEntity entity) {
    return Container(
      padding: const EdgeInsets.all(12.0),
      decoration: const BoxDecoration(
        border: Border(bottom: BorderSide(color: Colors.grey, width: 1.0)),
      ),
      child: Padding(
        padding: const EdgeInsets.only(bottom: 8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text("〒${entity.zipcode}"),
            Text("${entity.kana1} ${entity.kana2} ${entity.kana3}"),
            Text("${entity.address1} ${entity.address2} ${entity.address3}"),
          ],
        ),
      ),
    );
  }
}
ViewModel
import 'package:flutter_demo_application/Entity/zip_cloud_entity.dart';
import 'package:flutter_demo_application/repository/home_repository.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final homeViewModelNotifierProvider = FutureProvider<List<ZipCloudEntity>>((
  ref,
) async {
  final viewModel = await HomeViewModel(
    repository: ref.read(homeRepositoryProvider),
  );
  await viewModel.getZipCloudData();
  return viewModel.zipCloudData;
});
final homeViewModelStateNotifierProvider =
    StateNotifierProvider<ZipCloudEntityStateNotifier, List<ZipCloudEntity>>((
      ref,
    ) {
      return ZipCloudEntityStateNotifier(ref);
    });
class ZipCloudEntityStateNotifier extends StateNotifier<List<ZipCloudEntity>> {
  ZipCloudEntityStateNotifier(this.ref) : super(<ZipCloudEntity>[]);
  final Ref ref;
  Future<List<ZipCloudEntity>> getStateZipCloudData() {
    final repository = ref.read(homeRepositoryProvider);
    final homeModel = repository.getZipCloudData();
    return homeModel;
  }
}
class HomeViewModel {
  final HomeRepository repository;
  HomeViewModel({required this.repository});
  late List<ZipCloudEntity> _zipCloudData;
  List<ZipCloudEntity> get zipCloudData => _zipCloudData;
  bool isLoading = false;
  Future getZipCloudData() async {
    try {
      final data = await repository.getZipCloudData();
      _zipCloudData = data;
    } on Exception catch (exception) {
      Exception(exception);
    }
  }
}
Repository
import 'package:flutter_demo_application/Entity/zip_cloud_entity.dart';
import 'package:flutter_demo_application/service/home_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final homeRepositoryProvider = Provider<HomeRepository>(
  (ref) => HomeRepository(service: ref.read(homeServiceProvider)),
);
class HomeRepository {
  final HomeService service;
  HomeRepository({required this.service});
  Future<List<ZipCloudEntity>> getZipCloudData() async {
    try {
      final data = await service.getZipCloudDataService();
      return data;
    } on Exception catch (exception) {
      throw Exception(exception);
    }
  }
}
Service
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter_demo_application/Entity/zip_cloud_entity.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final homeServiceProvider = Provider<HomeService>((ref) {
  return HomeService();
});
class HomeService {
  Future<List<ZipCloudEntity>> getZipCloudDataService() async {
    try {
      String dummy = await rootBundle.loadString("json/dummy.json");
      final response = json.decode(dummy);
      response.forEach((key, value) {
        dummy = '$key: $value \x0A';
        return dummy;
      });
      final List<dynamic> responseData = response['results'];
      List<ZipCloudEntity> zipCloudList = <ZipCloudEntity>[];
      for (dynamic data in responseData) {
        final model = ZipCloudEntity.fromData(data);
        zipCloudList.add(model);
      }
      return zipCloudList;
    } on Exception catch (exception) {
      throw Exception(exception);
    }
  }
}
デモ動画
詳細
まず、JSONファイルとレスポンスクラスに関しては割愛する。
HomeService
ここではjsonファイルから直接する取得する処理を行っている。
まず、jsonをデータにする
String dummy = await rootBundle.loadString("json/dummy.json");
final response = json.decode(dummy);そのあと、jsonデータを連想配列に変換する
response.forEach((key, value) {
    dummy = '$key: $value \x0A';
    return dummy;
});それをレスポンスクラスに格納する。
final List<dynamic> responseData = response['results'];
List<ZipCloudEntity> zipCloudList = <ZipCloudEntity>[];
for (dynamic data in responseData) {
    final model = ZipCloudEntity.fromData(data);
    zipCloudList.add(model);
}
return zipCloudList;そして、上記の実装をしているメソッドを呼び出すために、HomeServiceのインスタンスを返すProviderのオブジェクトを作成する。
こうすることで他のクラスからHomeServiceを参照できるようにする。
final homeServiceProvider = Provider<HomeService>((ref) {
  return HomeService();
});HomeRepository
まず、メンバ変数としてHomeServiceを定義する。これを使ってHomeServiceで定義したデータを取得するメソッドを呼び出す。
そして、インスタンスメソッドにはHomeServiceをパラメータに設定する。
class HomeRepository {
  final HomeService service;
  HomeRepository({required this.service});
  // 省略
}そして、このRepositoryを他のクラスから呼び出せるようにProviderのオブジェクトを作成する。
Provider<HomeService>をreadメソッドで読み込むことで初期化に必要なHomeServiceオブジェクトを作成するわけですね。
final homeRepositoryProvider = Provider<HomeRepository>(
  (ref) => HomeRepository(service: ref.read(homeServiceProvider)),
);HomeViewModel
まず根本はRepositoryと同じ。
class HomeViewModel {
  final HomeRepository repository;
  HomeViewModel({required this.repository});
  // 省略
}ViewModelを呼び出せるように以下のようにProviderのオブジェクトを作成する。
FutureProviderはViewに表示するためのもの。
ここではzipCloudDataを取得する。取得したものはHomeViewでViewを生成するのに使われている。
final homeViewModelNotifierProvider = FutureProvider<List<ZipCloudEntity>>((
  ref,
) async {
  final viewModel = await HomeViewModel(
    repository: ref.read(homeRepositoryProvider),
  );
  await viewModel.getZipCloudData();
  return viewModel.zipCloudData;
});StateNotifierProviderは取得したデータを変数として返し、値が更新されたらViewに反映させるような動作を行う。
StateNotifier<List<ZipCloudEntity>>を継承したクラスを作成する。
StateNotifierで値が更新されたら画面に反映させるようにする。
そして、その更新されたら画面に反映されるデータをgetStateZipCloudDataメソッドで取得する。わかるとおり、ここでRepositoryのメソッドを呼び出す。
final homeViewModelStateNotifierProvider =
    StateNotifierProvider<ZipCloudEntityStateNotifier, List<ZipCloudEntity>>((
      ref,
    ) {
      return ZipCloudEntityStateNotifier(ref);
    });
class ZipCloudEntityStateNotifier extends StateNotifier<List<ZipCloudEntity>> {
  ZipCloudEntityStateNotifier(this.ref) : super(<ZipCloudEntity>[]);
  final Ref ref;
  Future<List<ZipCloudEntity>> getStateZipCloudData() {
    final repository = ref.read(homeRepositoryProvider);
    final homeModel = repository.getZipCloudData();
    return homeModel;
  }
}StateNotifierProviderの二つの型は、一つ目が返すオブジェクトの型、二つ目が継承するStateNotifierクラスの型らしい。
つまり、メソッドで返すデータの型。
ViewModelの処理内容を見ていこう
まずは以下。取得したデータの変数、そしてロード中かどうかを示すフラグだ。
late List<ZipCloudEntity> _zipCloudData;
List<ZipCloudEntity> get zipCloudData => _zipCloudData;
bool isLoading = false;そして、以下の箇所でRepositoryのメソッドを呼び出す。
取得したデータはViewで使用する。
Future getZipCloudData() async {
  try {
    final data = await repository.getZipCloudData();
    _zipCloudData = data;
  } on Exception catch (exception) {
    Exception(exception);
  }
}HomeView
取得したデータは以下の変数に格納する。
List<ZipCloudEntity> zipCloudList = <ZipCloudEntity>[];そして、データの取得、そして
ボタンを押下したら、データを取得する処理を行う。
onPressedはボタンを押下した時の処理。
var button = OutlinedButton(
  onPressed: () {
    Future<List<ZipCloudEntity>> zipCloudStateProvider =
      ref.watch(homeViewModelStateNotifierProvider.notifier).getStateZipCloudData();
        zipCloudStateProvider.then((list) {
          zipCloudList = list;
          ref.invalidate(homeViewModelNotifierProvider);
        });
      },
// 省略以下でデータの再取得を行う。
ref.watch(homeViewModelStateNotifierProvider.notifier).getStateZipCloudData();そして、以下で値の更新を行う。
invalidateメソッドは更新をして画面の再読み込みを行う。こうすることでデータを取得したら画面を更新する処理を行う。
zipCloudStateProvider.then((list) {
  zipCloudList = list;
  ref.invalidate(homeViewModelNotifierProvider);
});取得したデータの表示部分は以下。
watchメソッドで値を監視し、更新されたタイミングでリストの更新を行っている。
ref.watch(homeViewModelNotifierProvider)
  .when(
    data: (_) => ListView.builder(
      scrollDirection: Axis.vertical,
      shrinkWrap: true,
      itemCount: zipCloudList.length,
      itemBuilder: (_, index) {
        final zipCloud = zipCloudList[index];
        return zipCloudCell(zipCloud);
      },
    ),
    error: (error, _) => const Center(child: Text('通信エラー')),
    loading: () => const Center(child: CircularProgressIndicator()),
  ),参考ページ
- pub.dev「flutter_riverpod」
- Qiita「[Flutter]MVVM + Repositoryパターンでニュースアプリを開発した(はじめてのRiverPod)」
- Riverpod公式ドキュメント「プロバイダの利用方法」
- Zenn「Riverpodとは」
- Zenn「Riverpod を活用する」
- Zenn「StateNotifierの使い方」
- Zenn「Riverpodの内部実装を見てみよう②(ref.watch編)」
- Pentagon Blog「【Flutter】AsyncNotifierProviderを用いたUI更新」
- Qiita「【Flutter】Dartの非同期処理(Future・then)」
- Qiita「Flutter RiverpodのProviderのドキュメントメモ」
- Qiita「【Flutter】FutureProviderの再読み込みについて」
