目次
概要
開発コストを低くするためにも、そして複数名のエンジニアとチームを組んで開発する上でもアーキテクチャを意識した開発は大切だ。求められるエンジニアになるためにも。
アーキテクチャは様々だが、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の再読み込みについて」