Last active
May 25, 2020 14:04
-
-
Save albertusdev/e804dee4620505555bc1513cd0c0aa21 to your computer and use it in GitHub Desktop.
Base code for CRUD usecases in Flutter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:io'; | |
import 'package:dio/dio.dart'; | |
import 'package:meta/meta.dart'; | |
import 'errors.dart'; | |
import 'models/base_model.dart'; | |
import 'package:equatable/equatable.dart'; | |
import 'package:flutter/foundation.dart'; | |
abstract class BaseModel { | |
Map<String, dynamic> toJson() => {}; | |
} | |
abstract class BaseModelDeserializer<M extends BaseModel> { | |
M fromJson(Map<String, dynamic> json); | |
// Add every factory for models here | |
static Map<Type, BaseModelDeserializer> deserializers = { | |
LoginResponseModel: LoginResponseModelFactory.singleton, | |
}; | |
} | |
enum NetworkFailureErrorType { | |
noInternet, | |
badConnection, | |
server, | |
badRequest, | |
} | |
class NetworkFailureError extends Error { | |
final NetworkFailureErrorType type; | |
final String title; | |
final String description; | |
final String asset; | |
NetworkFailureError({this.title, this.description, this.asset, this.type}); | |
factory NetworkFailureError.noInternet(AppLocalization localization) => | |
NetworkFailureError( | |
title: 'Tidak ada koneksi Internet', | |
description: 'TODO', | |
); | |
factory NetworkFailureError.badConnection(AppLocalization localization) => | |
NetworkFailureError( | |
title: 'Koneksi Internet bermasalah', | |
description: 'TODO', | |
); | |
factory NetworkFailureError.server(AppLocalization localization) => | |
NetworkFailureError( | |
title: 'Server mengalami gangguan', | |
description: 'TODO', | |
); | |
} | |
class HttpClientProxy { | |
final Dio _httpClient; | |
final AppLocalization _localization; | |
HttpClientProxy({ | |
@required AppLocalization localization, | |
AppConfig config, | |
Dio httpClient, | |
}) : assert(localization != null), | |
_httpClient = httpClient ?? config.httpClient, | |
_localization = localization; | |
Future<Response<T>> _executeNetworkCall<T>( | |
Future<Response<T>> Function() networkCall) async { | |
try { | |
return await networkCall(); | |
} on SocketException catch (socketException) { | |
print(socketException); | |
throw NetworkFailureError.noInternet(_localization); | |
} on HttpException catch (httpException) { | |
print(httpException); | |
throw NetworkFailureError.server(_localization); | |
} on DioError catch (dioError) { | |
print(dioError); | |
switch (dioError.type) { | |
case DioErrorType.CONNECT_TIMEOUT: | |
case DioErrorType.SEND_TIMEOUT: | |
case DioErrorType.RECEIVE_TIMEOUT: | |
throw NetworkFailureError.badConnection(_localization); | |
case DioErrorType.DEFAULT: | |
throw NetworkFailureError.noInternet(_localization); | |
case DioErrorType.RESPONSE: | |
default: | |
if (dioError.response.statusCode == HttpStatus.badRequest) { | |
throw NetworkFailureError( | |
title: _localization.common.errorText, | |
// TODO(albertusangga) Do better parsing on bad request error message | |
description: dioError.response.data.toString(), | |
type: NetworkFailureErrorType.badRequest, | |
); | |
} | |
throw NetworkFailureError.server(_localization); | |
} | |
} | |
} | |
/// Sends HTTP GET request to [path] | |
Future<Response<Map<String, dynamic>>> get( | |
String path, { | |
Map<String, String> queryParameters, | |
Options options, | |
CancelToken cancelToken, | |
ProgressCallback onReceiveProgress, | |
}) async { | |
return await _executeNetworkCall(() async { | |
return await _httpClient.get<Map<String, dynamic>>( | |
path, | |
queryParameters: queryParameters, | |
options: options, | |
cancelToken: cancelToken, | |
onReceiveProgress: onReceiveProgress, | |
); | |
}); | |
} | |
/// Performs HTTP GET and automatically deserialize it as a Model | |
/// Example call: | |
/// | |
/// ```dart | |
/// final Product product = | |
/// httpClient.getAs<Product>('/product/1', deserializer: ProductDeserializer()); | |
/// ``` | |
Future<M> getAs<M extends BaseModel>( | |
String path, { | |
@required BaseModelDeserializer<M> deserializer, | |
Map<String, String> queryParameters, | |
Options options, | |
CancelToken cancelToken, | |
ProgressCallback onReceiveProgress, | |
}) async { | |
assert(deserializer != null); | |
final response = await get( | |
path, | |
queryParameters: queryParameters, | |
options: options, | |
cancelToken: cancelToken, | |
onReceiveProgress: onReceiveProgress, | |
); | |
return deserializer.fromJson(response.data); | |
} | |
/// Sends HTTP GET to HTTP endpoint that returns an array instead of json | |
Future<Response<List<dynamic>>> getList( | |
String path, { | |
Map<String, String> queryParameters, | |
Options options, | |
CancelToken cancelToken, | |
ProgressCallback onReceiveProgress, | |
}) async { | |
return await _executeNetworkCall(() async { | |
return await _httpClient.get( | |
path, | |
queryParameters: queryParameters, | |
options: options, | |
cancelToken: cancelToken, | |
onReceiveProgress: onReceiveProgress, | |
); | |
}); | |
} | |
/// Similar to [getList] but automatically deserializes it as List<M> | |
/// Example call: | |
/// | |
/// ```dart | |
/// final List<Product> products = httpClientProxy.getListAs<Product>( | |
/// '/products', | |
/// deserializer: ProductDeserializer(), | |
/// ); | |
/// ``` | |
Future<List<M>> getListAs<M extends BaseModel>( | |
String path, { | |
@required BaseModelDeserializer<M> deserializer, | |
Map<String, String> queryParameters, | |
Options options, | |
CancelToken cancelToken, | |
ProgressCallback onReceiveProgress, | |
}) async { | |
assert(deserializer != null); | |
final response = await getList( | |
path, | |
queryParameters: queryParameters, | |
options: options, | |
cancelToken: cancelToken, | |
onReceiveProgress: onReceiveProgress, | |
); | |
return response.data.map((json) => deserializer.fromJson(json)).toList(); | |
} | |
/// Sends HTTP POST request to [path] | |
Future<Response<Map<String, dynamic>>> post( | |
String path, { | |
data, | |
Map<String, dynamic> queryParameters, | |
Options options, | |
CancelToken cancelToken, | |
ProgressCallback onSendProgress, | |
ProgressCallback onReceiveProgress, | |
}) async { | |
return await _executeNetworkCall(() async { | |
return await _httpClient.post( | |
path, | |
data: data, | |
queryParameters: queryParameters, | |
options: options, | |
cancelToken: cancelToken, | |
onSendProgress: onSendProgress, | |
onReceiveProgress: onReceiveProgress, | |
); | |
}); | |
} | |
/// Sends HTTP POST request and automatically deserialize the JSON as model | |
/// Example call: | |
/// | |
/// ```dart | |
/// final user = httpClientProxy.postAs<User>( | |
/// '/login/', | |
/// data: {'username': 'username', 'password': 'password'}, | |
/// deserializer: UserDeserializer(), | |
/// ); | |
/// ``` | |
Future<M> postAs<M extends BaseModel>( | |
String path, { | |
@required BaseModelDeserializer<M> deserializer, | |
data, | |
Map<String, dynamic> queryParameters, | |
Options options, | |
CancelToken cancelToken, | |
ProgressCallback onSendProgress, | |
ProgressCallback onReceiveProgress, | |
}) async { | |
assert(deserializer != null); | |
final response = await post( | |
path, | |
data: data, | |
queryParameters: queryParameters, | |
options: options, | |
cancelToken: cancelToken, | |
onSendProgress: onSendProgress, | |
onReceiveProgress: onReceiveProgress, | |
); | |
return deserializer.fromJson(response.data); | |
} | |
void setAuthorizationToken(String token) { | |
_httpClient.options.headers[HttpHeaders.authorizationHeader] = 'JWT $token'; | |
} | |
void clearToken() { | |
_httpClient.options.headers.remove(HttpHeaders.authorizationHeader); | |
} | |
} | |
enum NetworkState { initialized, loading, ready, failed } | |
abstract class BaseNetworkService with ChangeNotifier { | |
BaseNetworkService(this.httpClient); | |
final HttpClientProxy httpClient; | |
NetworkState _state = NetworkState.initialized; | |
NetworkState get state => _state; | |
bool get isLoading => state == NetworkState.loading; | |
bool get isReady => state == NetworkState.ready; | |
bool get isFailed => state == NetworkState.failed; | |
@protected | |
set state(NetworkState state) { | |
_state = state; | |
if (!disposed) notifyListeners(); | |
} | |
void reset() { | |
_state = NetworkState.initialized; | |
} | |
NetworkFailureError _error; | |
NetworkFailureError get error => _error; | |
@protected | |
set error(NetworkFailureError error) { | |
_error = error; | |
state = NetworkState.failed; | |
} | |
bool _disposed = false; | |
bool get disposed => _disposed; | |
@override | |
void dispose() { | |
super.dispose(); | |
_disposed = true; | |
} | |
@override | |
void notifyListeners() { | |
if (!_disposed) { | |
super.notifyListeners(); | |
} | |
} | |
@protected | |
Future<T> executeNetworkCall<T>(Future<T> Function() networkCall) async { | |
state = NetworkState.loading; | |
try { | |
final result = await networkCall(); | |
state = NetworkState.ready; | |
return result; | |
} on NetworkFailureError catch (error) { | |
this.error = error; | |
} | |
return null; | |
} | |
Future<T> fetch<T extends BaseModel>({ | |
@required String path, | |
Map<String, String> queryParameters, | |
BaseModelDeserializer<T> deserializer, | |
}) async { | |
return await executeNetworkCall(() => httpClient.getAs<T>( | |
path, | |
queryParameters: queryParameters, | |
deserializer: deserializer ?? BaseModelDeserializer.deserializers[T], | |
)); | |
} | |
Future<T> submit<T extends BaseModel>({ | |
@required String path, | |
Map<String, dynamic> data, | |
BaseModelDeserializer<T> deserializer, | |
}) async { | |
return await executeNetworkCall(() => httpClient.postAs<T>( | |
path, | |
data: data, | |
deserializer: deserializer ?? BaseModelDeserializer.deserializers[T], | |
)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment