network
์ฉ๋
์๋ฒ์ ํต์ ํ๊ธฐ ์ํจ์ด๋ค. ํต์ ์ ํ ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ํค๋์ ์ค์ด์ ์ ๋ฌํด์ผํ ์ ๋ณด๋ค๋ ์๊ณ , ์ธํฐ์ ํฐ๋ฅผ ํ์ฉํด์ ์๋ฒ์ ์๋ต์ ๋ฐ๋ผ ์ ์ฒ๋ฆฌ๊ฐ ํ์ํ ๊ฒฝ์ฐ๋ ์๋ค. ์ด ๋ชจ๋ ์ฌํญ์ ๋์ํ๊ธฐ ์ํ ๋ชจ๋์ด๋ค.
๊ตฌํ
dio ๋ฅผ ์ฌ์ฉํ๋ค.
$ flutter pub add dio

data ๋ ์ด์ด ๋ด์ network ๋ง๊ณ response, request, repository ๋ฅผ ๋ ์ ์๋ค. ๋์ค์ domain ์์ญ์์๋ ์ ๋ฆฌํ๊ฒ ์ง๋ง repository ์ ๊ฒฝ์ฐ domain ๋ ์ด์ด์ ์ธํฐํ์ด์ค๋ฅผ ๋๊ณ data ๋ ์ด์ด์์ ๊ตฌํ์ฒด๋ฅผ ๋ง๋ค์ด์ ์ฌ์ฉํ๋ ๊ฒ์ด ์๊ฒฉํ๊ฒ ํด๋ฆฐ ์ํคํ ์ฒ๋ฅผ ๊ตฌํํ๋ ๊ฒ์ด๋ค. ํ์ง๋ง ํ๋ก์ ํธ๋ฅผ ํด๋ณด๋ ๋ฐ์ดํฐ ์์ฑ ์ ๋ต์ด ์ ์ฒด์ ์ผ๋ก ๋ฐ๋๋ค๋๊ฐ ํ์ง ์๋ ์ด์ ๋ถํ์ํ๊ฒ ๊ณ์ธต์ ๋๋๋ ํ์๋ผ๊ณ ๋๊ปด์ก๋ค. ๊ฒฐ๋ก ์ ์ผ๋ก repository ๋ data ๋ ์ด์ด์๋ง ๋ฐ๋ก ๊ตฌํ์ฒด๋ฅผ ๋๋ ๊ฒ์ด ์ข์ ๊ฒ ๊ฐ๋ค.
import 'base_http_client.dart';
class ApiClient extends BaseHttpClient {
ApiClient({
required super.clientBaseUrl,
required super.customInterceptors,
super.customOptions,
});
}
import 'package:dio/dio.dart';
import '../../../application/environments/environment.dart';
abstract class BaseHttpClient with DioMixin implements Dio {
final String clientBaseUrl;
final List<Interceptor> customInterceptors;
BaseOptions? customOptions;
BaseHttpClient({
required this.clientBaseUrl,
required this.customInterceptors,
this.customOptions,
}) : super() {
if (customOptions != null) {
options = customOptions!;
}
httpClientAdapter = HttpClientAdapter();
options = BaseOptions(
baseUrl: clientBaseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
);
if (Environment.isDevelopment) {
interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
}
interceptors.addAll(customInterceptors);
}
}
์์ ๊ฐ์ด ์์ ํด์ฃผ๋ฉด ํด๋ผ์ด์ด์ธํธ ์ ์๋ ๋์ด ๋๋ค. ์๋๋ ๋ด๊ฐ ์ฌ์ฉํ ์ธํฐ์ ํฐ๋ค. ๊ธฐ๋ณธ์ ์ธ ๊ฒ๋ค์ด๋ค.
BaseHeaderInterceptor ๋ ํค๋์ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ด์ ๋ด์ฉ๋ค์ ์ธํ ํ๋ค.
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
// import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../../../barrel.dart';
class BaseHeaderInterceptor extends InterceptorsWrapper {
@override
void onRequest(
RequestOptions options, RequestInterceptorHandler handler) async {
await _setHeaderWithAccessToken(options.headers);
await _setHeaderWithPackageInfo(options.headers);
await _setHeaderWithDeviceInfo(options.headers);
// await _setHeaderWithPushToken(options.headers);
super.onRequest(options, handler);
}
Future<void> _setHeaderWithAccessToken(Map<String, dynamic> headers) async {
final String? accessToken = await AccessManager.getAccessToken();
if (accessToken == null) {
return;
}
const String ACCESS_TOKEN_PREFIX = 'Bearer';
const String SPACE = ' ';
headers['access-token'] = ACCESS_TOKEN_PREFIX + SPACE + accessToken;
}
Future<void> _setHeaderWithPackageInfo(Map<String, dynamic> headers) async {
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
headers['app-name'] = packageInfo.appName;
headers['package-name'] = packageInfo.packageName;
headers['version'] = packageInfo.version;
headers['build-number'] = packageInfo.buildNumber;
}
Future<void> _setHeaderWithDeviceInfo(Map<String, dynamic> headers) async {
final DeviceInfoPlugin deviceInfo = Injector.deviceInfoPlugin;
if (Platform.isAndroid) {
final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
headers['uuid'] = androidInfo.id;
headers['operating-system'] = 'Android';
headers['operating-system-version'] =
androidInfo.version.sdkInt.toString();
return;
}
if (Platform.isIOS) {
final IosDeviceInfo iOSInfo = await deviceInfo.iosInfo;
headers['uuid'] = iOSInfo.identifierForVendor;
headers['operating-system'] = 'iOS';
headers['operating-system-version'] = iOSInfo.systemVersion;
}
}
// Future<void> _setHeaderWithPushToken(Map<String, dynamic> headers) async {
// String? pushToken = await FirebaseMessaging.instance.getToken();
// headers['push-token'] = pushToken ?? '';
// }
}
TokenInterceptor ๋ ํ ํฐ ๋ง๋ฃ์ ๋ฐ๋ก ์๋ฌ๋ฅผ ๋ด๋ ๊ฒ์ด ์๋๋ผ ๋ฆฌํ๋ ์ ํ ํฐ์ ์ด์ฉํด์ ๋ค์ ํ ํฐ์ ์ฌ๋ฐ๊ธ๋ฐ์์ ๊ธฐ์กด์ ์์ฒญ์ ๊ทธ๋๋ก ์ํํ๋ ๊ฒ์ด๋ค. ์๋ฒ์์ ํ ํฐ์ด ๋ง๋ฃ์ผ๋ ์๋์ ๊ฐ์ด ์ฝ์๋ ์ฝ๋์ status๋ฅผ ๋ด๋ ค์ฃผ๊ณ ๋ ์กฐ๊ฑด์ด ๋ชจ๋ ๋ง์กฑํ ๋ ์ฌ๋ฐ๊ธ ์ฒ๋ฆฌ๋ฅผ ํ๋ค. ๋๋ ์๋ฒ์์๋ ๋ฆฌํ๋ ์ ํ ํฐ์ ๋ณด๊ดํ๋๋ก ํด์ ๋ณด์์ ๊ฐํ ํ๋ ์ชฝ์ผ๋ก ๊ตฌํํ๋ค.
import 'package:dio/dio.dart';
import '../../../barrel.dart';
class TokenInterceptor extends InterceptorsWrapper {
TokenInterceptor();
@override
Future<void> onError(
DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401 &&
err.response?.data['code'] == 'AU01') {
final String? refreshToken = await AccessManager.getRefreshToken();
if (refreshToken != null) {
try {
Response response = await Injector.apiClient
.put('/user/token/refresh?refreshToken=$refreshToken');
final String newAccessToken = response.data['accessToken'];
final String newRefreshToken = response.data['refreshToken'];
await AccessManager.replaceTokens(
accessToken: newAccessToken, refreshToken: newRefreshToken);
// ์๋์ ์์ฒญ์ ์ฌ์คํ
RequestOptions requestOptions = err.requestOptions;
requestOptions.headers['access-token'] = 'Bearer $newAccessToken';
Response newResponse = await Injector.apiClient.request(
requestOptions.path,
queryParameters: requestOptions.queryParameters,
options: Options(
method: requestOptions.method,
headers: requestOptions.headers,
responseType: requestOptions.responseType,
contentType: requestOptions.contentType,
extra: requestOptions.extra,
),
data: requestOptions.data,
);
handler.resolve(newResponse);
} catch (exception) {
// ํ ํฐ ์ฌ๋ฐ๊ธ ์คํจ์ ๋ก์ง ์ฒ๋ฆฌ
handler.next(err);
}
}
} else {
handler.next(err);
}
}
}
์๋๋ ์์คํ ์ ๊ฒ์ค์ผ๋ ์์คํ ์ ๊ฒ์ค์ด๋ผ๋ ๋ค์ด์ผ๋ก๊ทธ๋ฅผ ๋์ฐ๊ณ ์ ํ์ง ์์ด ์ฑ์ ์ข ๋ฃ์ํค๋๋ก ํ๋ ์ธํฐ์ ํฐ์ด๋ค. ํ ํฐ ์ธํฐ์ ํฐ์ ๋ง์ฐฌ๊ฐ์ง๋ก ์๋ฒ์์ ์ ์๋ ์ฝ๋์ status ๋ฅผ ๋ด๋ ค์ฃผ๊ฒ ํ๋ค.
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import '../../../barrel.dart';
class ServerMaintenanceInterceptor extends Interceptor {
ServerMaintenanceInterceptor();
@override
Future<void> onError(
DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 503 &&
err.response?.data['code'] == 'SY01') {
BasicDialog.show(
context: RouterConfiguration.navigatorKey.currentContext!,
title: TextManager.serviceMaintenanceTitle.tr(),
contents: TextManager.serviceMaintenanceMessage.tr(),
confirmAction: () {
exit(0);
});
} else {
handler.next(err);
}
}
}
์ฌ์ฉ
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter_template/barrel.dart';
import 'package:get_it/get_it.dart';
class Injector {
Injector._();
static DeviceInfoPlugin get deviceInfoPlugin =>
GetIt.instance.get<DeviceInfoPlugin>();
static Dio get apiClient => GetIt.instance.get<Dio>();
static Future registerDependencies() async {
_registerUtils();
_registerNetworks();
_registerRepositories();
}
static _registerUtils() async {
GetIt.instance
.registerLazySingleton<DeviceInfoPlugin>(() => DeviceInfoPlugin());
}
static _registerNetworks() async {
GetIt.instance.registerLazySingleton<Dio>(
() => ApiClient(
clientBaseUrl: Environment.baseUrl,
customInterceptors: [
BaseHeaderInterceptor(),
// TokenInterceptor(),
],
),
);
}
static _registerRepositories() async {}
}
์ฌ์ฉ์ ์์ ๊ฐ์ด Injector ์ ์ ์ํด๋๊ณ ํ์ํ ๋(= Bloc ์ ์ฃผ์ ๋๋ Repository ์ ์ฃผ์ ) ๊บผ๋ด์ ์ฌ์ฉํ๋ค.
Last updated