Dio+Retrofit: 導入 使い方 - Ki-Kobayashi/flutter_wiki GitHub Wiki
以下に加えて、「Freezed: 導入+使い方」の追加もしておく
fvm flutter pub add dio
fvm flutter pub add retrofit
fvm flutter pub add --dev retrofit_generator
dependencies:
flutter:
sdk: flutter
...
// https://pub.dev/packages/dio
dio: ^5.3.2 👈追加
// https://pub.dev/packages/retrofit
retrofit: ^4.0.1 👈追加
...
dev_dependencies:
flutter_test:
sdk: flutter
...
// https://pub.dev/packages/retrofit_generator
retrofit_generator: ^7.0.8 👈追加
...
.
Dtoの作成は、「Freezed: 導入+使い方」を参照
.
「envied: 導入+使い方」を参照
.
下記は、「Riverpod」導入済が前提
📚 Headerなどの設定参考:https://github.com/trevorwang/retrofit.dart/issues/190#issuecomment-1719090470
📚 リトライ処理:https://zenn.dev/minma/articles/5e611fc92b9e1f
📚 アクセストークンの有効期限が切れた時に更新: https://tech-blog.yayoi-kk.co.jp/entry/2023/05/23/110000#%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE%E5%8F%96%E5%BE%97
📚 上記のGithub:https://github.com/standfirm/misoca-sample-flutter/blob/main/lib/general/comm/authorization_interceptor.dart
.
@Riverpod(keepAlive: true)
Dio dio(DioRef ref) {
var dio = Dio()
..options = BaseOptions(
baseUrl: Env.baseUrl,
contentType: Headers.jsonContentType,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
);
dio.options.headers[Env.apiKeyName] = Env.apiKey;
dio.interceptors
..add(LogInterceptor())
..add(InterceptorsWrapper(
onRequest: (options, handler) async {
switch (options.path) {
// ログイン確認が必要な API Request
case '/setting/profile':
case '/mypage':
// 最新のログイン情報(sessionIDの有効性)をチェック & ログインステータス更新
await ref.read(authenticationNotifierProvider.notifier).checkLoggedIn();
// ログインしていなければリクエストしない
if (!await ref.read(isLoggedInProvider)) {
return;
}
}
final sessionId =
await ref.read(secureStorageRepositoryProvider).readSessionId();
if (sessionId != null) {
options.headers['Cookie'] = 'id=$sessionId';
}
return handler.next(options);
},
onResponse: (Response<dynamic> response, handler) async {
List<String>? rowCookies = response.headers['set-cookie'];
if (rowCookies != null) {
String sessionId = _extractSessionId(rowCookies);
if (sessionId != '') {
await _saveSessionIds(ref, sessionId);
}
response.statusCode;
}
return handler.next(response);
},
));
return dio;
}
// Set-CookieからセッションID抽出
String _extractSessionId(List<String> rawCookies) {
final sessionIdPart = rawCookies
.map((rawCookie) {
return rawCookie.split(';')[0];
})
.toList()
.first;
// 不要箇所の除去
if (sessionIdPart.startsWith('id=')) {
return sessionIdPart.replaceFirst('id=', '');
}
return '';
}
// セッションIDの保存
Future<void> _saveSessionIds(DioRef ref, String sessionId) async {
final secureStorageRepository = ref.read(secureStorageRepositoryProvider);
logger.d("@@@@@@@@ XXX start save Session ID : $sessionId");
if (await secureStorageRepository
.containsKeyInSecureData(SharedPrefKeyConsts.sessionIdKey)) {
await secureStorageRepository.deleteSessionId();
}
await secureStorageRepository.writeSessionId(id: sessionId);
}
.
ApiResponse: API側から、たとえば以下のように固定のパラメタのResponse形式で返される場合のみ、ラップするもの <※一般的にラップされない:Header情報に statuscode / message が含まれるため、重複するようだ。>
- statusCode
- message
- payload
.
part 'example_rest_client.g.dart';
// 🚨以下は、repositoryIFに定義しておくと良き
@riverpod
ExampleRestClient exampleRestClient(ExampleRestClientRef ref) {
return ExampleRestClient(ref.read(dioProvider));
}
@RestApi()
abstract class ExampleRestClient implements ExampleDatasource {
factory ExampleRestClientt(Dio dio) = _ExampleRestClient;
@override
@GET('/items')
Future<ApiResponse<List<Item>>> getItemList();
@override
@POST('/example/register')
Future<ApiResponse<ExampleResponse>> registerExample(
@Body() RegisterExampleRequest requestBody,
);
@POST('/item/delete')
Future<ApiResponse<String>> deleteItem(
@Body() DeleteItemRequest requestBody,
);
}
API接続処理を、
- Mock用 の Jsonファイル から取得する DataSource
- サーバーサイドの API 接続から取得する DataSource
で分けたい場合は、以下のように抽象クラス(Interface)を作成しておくと良い
.
abstract class ExampleDatasource {
Future<ApiResponse<List<Item>>> getItemList();
Future<ApiResponse<ExampleResponse>> registerExample(
RegisterExampleRequest requestBody,
);
Future<ApiResponse<String>> deleteItem(
DeleteItemRequest requestBody,
);
}
Mock用のJsonファイルは、「assets/mock/ココ」に配置し、
pubspec.yaml に 該当ディレクトリを読み込めるよう、必要設定を追記しておく
.
下記は、MockJsonファイルを読み込むための、MockDataSourceの一例
.
abstract class MockExampleDatasource {
Future<List<Item>> fetchItemList() async {
final content = await json.decode(await rootBundle
.loadString('assets/stub/stub_items.json'));
final res = ApiResponse.fromJson(
content,
(Object? json) => ItemList.fromJson(json as Map<String, dynamic>),
);
return res.payload.itemList ?? [];
}
}
.
.
part 'example_repository.g.dart';
// 👇以下追加
@riverpod
ExampleDatasource exampleDatasource(ExampleDatasourceRef ref) {
throw UnimplementedError();
}
// 👇以下のRepository に Datasourceをセット
@riverpod
ExampleRepository exampleRepository(ExampleRepositoryRef ref) {
final repository = ExampleRepository(client: ref.read(exampleDatasourceProvider));
return repository ;
}
class ExampleRepository {
ExampleRepository({
required ExampleDatasource client,
}) : _client = client;
final ExampleDatasource _client;
Future<ExampleResponse> registerExample({
required RegisterExampleRequest requestBody,
}) async {
try {
final res = await _client.registerExample(requestBody);
if (res.statusCode != ApiStatusCode.success.statusCode) {
// エラー情報を表示するログを入れてもOK
throw NetworkError.getApiError(
ApiStatusCodeError(apiStatusCode: res.statusCode),
);
}
return res.payload;
} on Exception catch (e) {
throw NetworkError.getApiError(e);
}
}
.
Future<void> main() async {
// WidgetsBindingはUIの構築やライフサイクルイベントの管理()
// runApp()を呼び出す前に初期化する必要がある際に用いる(非同期処理や外部サービスの初期化が必要な場合は必須)
// 非同期処理が完了する前にウィジェットがビルドされることを防ぐ
WidgetsFlutterBinding.ensureInitialized();
// アプリを縦方向に固定する
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
// 〇日前表示設定
timeago.setLocaleMessages("ja", timeago.JaMessages());
// google fontsの設定
GoogleFonts.config.allowRuntimeFetching = kDebugMode;
if (isRelease) {
isDevExample = false;
}
runApp(
ProviderScope(
overrides: [
flutterSecureStorageProvider.overrideWithValue(
const FlutterSecureStorage(),
),
sharedPreferencesProvider.overrideWithValue(
await SharedPreferences.getInstance(),
),
// 👇以下追加
exampleDatasourceProvider.overrideWith((ref) => ref.watch(isDevExample
? stubExampleDatasourceProvider
: exampleRestClientProvider)),
],
child: const ExampleApp(),
),
);
}
.
下記は、見直しが必要
参考:https://gist.github.com/ashishrawat2911/1dcfde5d713f389dcebbd9a69b863145
参考:https://zenn.dev/dev_tatsuya/articles/cffaa7c50dfad7
参考:https://qiita.com/muttsu-623/items/2fa68fb6689c76f5415f
(Resultの方法は、RivepodのAsyncValue使用してたら、いらなそう。。。)
.
sealed class NetworkError implements Exception {
const NetworkError ();
static NetworkError getApiError(error) {
if (error is ApiStatusCodeError) {
return error;
}
if (error is Exception) {
try {
NetworkError error0 = const _UnexpectedException();
if (error is DioException) {
switch (error.type) {
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
logger.e('connection time out.', error: error, stackTrace: error.stackTrace);
error0 = const _SendTimeoutException();
break;
case DioExceptionType.cancel:
logger.e('connection canceled.', error: error, stackTrace: error.stackTrace);
error0 = const _RequestCancelledException();
break;
case DioExceptionType.badResponse:
final statusCode = error.response?.statusCode;
if (statusCode == null) break;
if (400 <= statusCode && statusCode < 500) {
logger.e('$statusCode:',
error: error, stackTrace: error.stackTrace);
error0 = const _RequestException();
} else if (500 <= statusCode) {
logger.e('$statusCode:',
error: error, stackTrace: error.stackTrace);
error0 = const _ServiceUnavailableException();
}
break;
case DioExceptionType.connectionError:
logger.e('Communication environment error: ${error.message}');
error0 = const _NoInternetConnectionException();
break;
default:
if (error.error is SocketException) {
logger.e('Communication environment error :${error.message}');
error0 = const _NoInternetConnectionException();
break;
}
error0 = const _UnexpectedException();
}
} else if (error is SocketException) {
logger.e('Communication environment error:${error.message}');
error0 = const _NoInternetConnectionException();
} else if (error is _NoInternetConnectionException) {
logger.e('Communication environment error:${error.errorMessage}');
error0 = const _NoInternetConnectionException();
} else {
logger.e('Unknown error :$error', error: error);
error0 = const _UnexpectedException();
}
return error0;
} catch (_) {
logger.e('Unknown error:$error');
return const _UnexpectedException();
}
} else {
logger.e('Unknown error :$error');
return const _UnexpectedException();
}
}
String get errorMessage => switch (this) {
ApiStatusCodeError(apiStatusCode: int apiStatusCode) =>
ApiStatusCode.getErrMessageFromStatusCode(apiStatusCode), 👈※次項の ApiStatusCode 参照
_RequestCancelledException() => StringConstants.errNetworkCancel,
_UnauthorisedRequestException() =>
StringConstants.errNetworkAuthentication,
_RequestException() => StringConstants.errNetworkServer,
_ServiceUnavailableException() => StringConstants.errNetworkServer,
_SendTimeoutException() => StringConstants.errNetwork,
_NoInternetConnectionException() => StringConstants.errNetwork,
_UnexpectedException() => StringConstants.errNetworkUnexpected,
};
}
final class ApiStatusCodeError extends NetworkError {
const ApiStatusCodeError({required this.apiStatusCode}); 👈※次項の ApiStatusCode 参照
final int apiStatusCode;
}
final class _RequestCancelledException extends NetworkError {
const _RequestCancelledException();
}
final class _UnauthorisedRequestException extends NetworkError {
const _UnauthorisedRequestException();
}
final class _RequestException extends NetworkError {
const _RequestException();
}
final class _ServiceUnavailableException extends NetworkError {
const _ServiceUnavailableException();
}
final class _SendTimeoutException extends NetworkError {
const _SendTimeoutException();
}
final class _NoInternetConnectionException extends NetworkError {
const _NoInternetConnectionException();
}
final class _UnexpectedException extends NetworkError {
const _UnexpectedException();
}
.
API返却値のステータスコードを転記し、メッセージはアプリ用に丸める
// TODO :
enum ApiStatusCode {
// なし
none(statusCode: 0),
// 成功
success(statusCode: 1),
// その他DBの致命的なエラー(DBにアクセスできないなど)
dbError(statusCode: 2),
// セッション取得エラー
userSessionCanNotGetError(
statusCode: 3,
message: StringConstants.errApiUserSessionCanNotGetError,
),
// 2段階登録/更新(確認コード利用)の有効期限切れ
userRegistrationExpires(
statusCode: 4,
message: StringConstants.errApiUserRegistrationExpires,
),
// ID または パスワード不一致
userLoginIncorrectIdOrPassword(
statusCode: 5,
message: StringConstants.errApiUserLoginIncorrectIdOrPassword,
),
・
・
・
const ApiStatusCode({
required this.statusCode,
this.message,
});
final int statusCode;
final String? message;
/// Enumの値を順次確認し、ステータスコード に紐づく エラーメッセージ を取得
static String getErrMessageFromStatusCode(int statusCode) {
final apiStatus = ApiStatusCode.values.firstWhere(
(apiStatusCode) => apiStatusCode._isMatchStatusCode(statusCode),
);
return apiStatus.message ?? "";
}
/// 引数のstatusCodeと同一か
bool _isMatchStatusCode(int statusCode) {
if (statusCode == this.statusCode) {
return true;
}
return false;
}
}
.
.
.