Skip to content

Instantly share code, notes, and snippets.

@albertusdev
Last active May 25, 2020 14:04
Show Gist options
  • Save albertusdev/e804dee4620505555bc1513cd0c0aa21 to your computer and use it in GitHub Desktop.
Save albertusdev/e804dee4620505555bc1513cd0c0aa21 to your computer and use it in GitHub Desktop.
Base code for CRUD usecases in Flutter
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