🤖

Retrofit + riverpod v2

2024/07/16に公開

対象者

  • riverpod v2を使ったことある人
  • API通信をやったことある人
  • Retrofitを使ったことがある人

こちらに完成品あります

https://pub.dev/packages/dio/install
https://pub.dev/packages/retrofit/install
https://pub.dev/packages/retrofit_generator/install

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の形に合わせて、モデルクラスを作りましょう。こんな感じで良いです。

lib/domain/post.dart
// 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メソッドを自動生成してくれます。

lib/api/post_api.dart
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でも十分ですが、新しい技術を学習する目的で使ってみます。

lib/api/post_async_notifier.dart
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を使うよりも分岐処理を簡潔に書けます。

lib/views/post_view.dart
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したデータを表示するクラスをインポートしたら完成です。

main.dart
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
Jboy王国メディア

Discussion