Retrofit + riverpod v2
対象者
- riverpod v2を使ったことある人
- API通信をやったことある人
- Retrofitを使ったことがある人
API通信が簡単にできるパッケージであるRetrofit
を使った記事となっております。自動生成形なので、未経験の人は混乱するでしょうから、まずは入門レベルのチュートリアルをやることをお勧めします。
プロジェクトの説明
過去に書いた記事をriverpod generatorに対応させた記事となっております。
これ
レイヤーはこのように分けております。
lib
├── api
│ ├── post_api.dart
│ ├── post_api.g.dart
│ ├── post_async_notifier.dart
│ └── post_async_notifier.g.dart
├── domain
│ ├── post.dart
│ ├── post.freezed.dart
│ └── post.g.dart
├── main.dart
└── views
└── post_view.dart
🎁add package
add retrofit:
flutter pub add dio && \
flutter pub add retrofit && \
dart pub add retrofit_generator
add: flutter_riverpod:
flutter pub add \
flutter_riverpod \
riverpod_annotation \
dev:riverpod_generator \
dev:build_runner \
dev:custom_lint \
dev:riverpod_lint
add: freezed:
flutter pub add \
freezed_annotation \
--dev build_runner \
--dev freezed \
json_annotation \
--dev json_serializable
Makefile
自動生成の便利なコマンドのMakefile
があります。こちら活用してみてください。プロジェクト直下に、Makefileというファイルを作成して、下記のコードを貼り付けるだけです。
.PHONY: setup
setup:
@flutter clean
@flutter pub get
.PHONY: br
br:
@flutter pub run build_runner watch --delete-conflicting-outputs
自動生成のコマンドは、make br
で実行します。
make br
📦Entity
APIのJSONの形に合わせて、モデルクラスを作りましょう。こんな感じで良いです。
// This file is "main.dart"
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'post.freezed.dart';
part 'post.g.dart';
class Post with _$Post {
const factory Post({
required int userId,
required int id,
required String title,
required String body,
}) = _Post;
factory Post.fromJson(Map<String, Object?> json) => _$PostFromJson(json);
}
Retrofit + Provider
Retrofitを実装したクラスとグローバルに使うためのプロバイダーを作成しましょう。メソッドを定義すると、HTTP GETメソッドを自動生成してくれます。
import 'package:dio/dio.dart';
import 'package:retrofil_example/domain/post.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:retrofit/http.dart';
part 'post_api.g.dart';
(keepAlive: true)
PostApi postApi(PostApiRef ref) {
return PostApi(Dio(
BaseOptions(
contentType: "application/json",
connectTimeout: const Duration(seconds: 10),
)
));
}
(baseUrl: 'https://jsonplaceholder.typicode.com/')
abstract class PostApi {
factory PostApi(Dio dio, {String baseUrl}) = _PostApi;
('/posts')
Future<List<Post>> getPosts();
}
⏰State
AsyncNotifier という Riverpod v2 から追加された非同期に状態管理ができるプロバイダーを使ってみようと思います。FutureProviderでも十分ですが、新しい技術を学習する目的で使ってみます。
import 'package:retrofil_example/api/post_api.dart';
import 'package:retrofil_example/domain/post.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'post_async_notifier.g.dart';
class PostAsyncNotifier extends _$PostAsyncNotifier {
FutureOr<List<Post>> build() {
return getPosts();
}
Future<List<Post>> getPosts() async {
return await ref.read(postApiProvider).getPosts();
}
}
🧙♀️View
AsyncValue
のデータを表示するには、switch
というものを使います。こんな感じで書いていただければ大丈夫です。FutureBuilderを使うよりも分岐処理を簡潔に書けます。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:retrofil_example/api/post_async_notifier.dart';
class PostView extends ConsumerWidget {
const PostView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final posts = ref.watch(postAsyncNotifierProvider);
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.indigo,
title: const Text('Posts'),
),
body: switch(posts) {
AsyncData(:final value) => ListView.builder(
itemCount: value.length,
itemBuilder: (context, index) {
final post = value[index];
return ListTile(
title: Text(post.title),
subtitle: Text(post.body),
);
},
),
AsyncError(:final value) => Center(child: Text('error $value')),
_ => const Center(child: CircularProgressIndicator()),
},
);
}
}
main.dart
に、HTTP GETしたデータを表示するクラスをインポートしたら完成です。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:retrofil_example/views/post_view.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const PostView(),
);
}
}
正しく設定できていれば表示できます🙌
感想
いかがでしたでしょうか?
http
というパッケージや普通のdio
を使えば、HTTP GETできるのですが、処理は書いておらず、自動生成されたものを使っております。OpenAPIと呼ばれているものでも同じことできるのですが、あれは私には難しすぎて、好みではなかったですね💦
自動生成されるとこんな感じです!
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'post_api.dart';
// **************************************************************************
// RetrofitGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element
class _PostApi implements PostApi {
_PostApi(
this._dio, {
this.baseUrl,
}) {
baseUrl ??= 'https://jsonplaceholder.typicode.com/';
}
final Dio _dio;
String? baseUrl;
Future<List<Post>> getPosts() async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _result =
await _dio.fetch<List<dynamic>>(_setStreamType<List<Post>>(Options(
method: 'GET',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/posts',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
))));
var _value = _result.data!
.map((dynamic i) => Post.fromJson(i as Map<String, dynamic>))
.toList();
return _value;
}
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||
requestOptions.responseType == ResponseType.stream)) {
if (T == String) {
requestOptions.responseType = ResponseType.plain;
} else {
requestOptions.responseType = ResponseType.json;
}
}
return requestOptions;
}
String _combineBaseUrls(
String dioBaseUrl,
String? baseUrl,
) {
if (baseUrl == null || baseUrl.trim().isEmpty) {
return dioBaseUrl;
}
final url = Uri.parse(baseUrl);
if (url.isAbsolute) {
return url.toString();
}
return Uri.parse(dioBaseUrl).resolveUri(url).toString();
}
}
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$postApiHash() => r'2cc2c6914c5394a083cf000dfff8b20b48d4aa19';
/// See also [postApi].
(postApi)
final postApiProvider = Provider<PostApi>.internal(
postApi,
name: r'postApiProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$postApiHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef PostApiRef = ProviderRef<PostApi>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
Discussion