I’ve decided to move on from Flutter professionally…but before I do, I wanted to make some posts about practices that I have implemented in my coding. This is mostly for personal use, in case I want to come back to Flutter and need a quick reminder, but maybe it might help others improve their coding skills too :)
Today, I will be showing my go-to file for HTTP requests, and some methods I use to ease the process of converting the response to classes. I will not be posting the full github repository as these are snippets from different projects.
Here’s my folder structure .
lib
- class
- providers
- repository
- connect (http request)
- custom_types (json, conversion methods)
- sqflite_repo
..etc
- service (methods to be utilized in provider)
- views
typedef Json<Key, Value> = Map<String, dynamic>;
enum ReqMethod {
get, post, put, delete, patch
}
----------------------------
Connect(){
if (kDebugMode || kProfileMode) {
this._baseUrl = "";
}
}
String _baseUrl = "";
String _path(String path){
String _path = path.trim();
if (!_path.startsWith("/")) _path = "/" + _path;
return _path;
}
Some basics:
1. typedef Json —shortens my coding from Map<String, dynamic> → Json everywhere
2. ReqMethod — enum for different types of http request methods, because I am not a fan of using String (eg. method: “POST”). This allows me to make ONE request method instead of making multiple methods for each http request method.
3. baseUrl — I set my baseUrl to dev server url when I’m developing or testing on profile mode, so that I don’t have to constantly change it
4. path() — we are bound to make mistakes, and this method allows all paths to start with “/” and trim for any spaces
Here is my all-in-one method for API requests. Because this method has a try catch block, you will not have to implement a try-catch anywhere else. There’s definitely room for improvement and please let me know of any suggestions!
/// default request method: get
Future<Json> reqAPIServer({
ReqMethod reqMethod = ReqMethod.get,
required String path,
void Function(ReqModel)? cb,
Map<String, String>? headers,
dynamic body,
required String? accessToken,
}) async {
try {
final Uri _url = Uri.parse(this._baseUrl + this._path(path));
final Map<String, String> _headers = {...headers ?? {}, "content-type":"application/json", if (accessToken != null) "authorization": "Bearer $accessToken"};
const Duration _timeOutLimit = Duration(seconds: 13);
FutureOr<http.Response> _onTimeOut() => http.Response(json.encode({"error": "timeout"}), 408);
http.Response? _res;
if (reqMethod == ReqMethod.get) {
_res = await http.get(_url, headers: _headers).timeout(_timeOutLimit, onTimeout: _onTimeOut);
} else if (reqMethod == ReqMethod.post) {
_res = await http.post(_url, headers: _headers, body: json.encode(body)).timeout(_timeOutLimit, onTimeout: _onTimeOut);
} else if (reqMethod == ReqMethod.delete) {
_res = await http.delete(_url, headers: _headers).timeout(_timeOutLimit, onTimeout: _onTimeOut);
} else if (reqMethod == ReqMethod.put) {
_res = await http.put(_url, headers: _headers, body: json.encode(body)).timeout(_timeOutLimit, onTimeout: _onTimeOut);
} else if (reqMethod == ReqMethod.patch) {
_res = await http.patch(_url, headers: _headers, body: json.encode(body)).timeout(_timeOutLimit, onTimeout: _onTimeOut);
}
if (_res == null) return {"error": "no valid request method provided"};
if (cb != null) cb(ReqModel(statusCode: _res.statusCode));
/// error from server
if (_res.headers["content-type"] == "text/html; charset=utf-8") return {"error": _res.body, "statusCode": _res.statusCode };
/// 한국어 때문에 utf8.decode()
final Json _decodedBody = json.decode(utf8.decode(_res.bodyBytes)) as Json;
if (kDebugMode|| kProfileMode) print("API RES: path - $path, body: $body");
/// the keys ("error", "response") will differ depending on your own server responses
if (_decodedBody.containsKey("error")) {
if (kDebugMode|| kProfileMode) print(_decodedBody["error"]);
return {"error": _decodedBody["error"], "statusCode": _res.statusCode };
}
if (kDebugMode|| kProfileMode) {
final String _resToString = _decodedBody["response"].toString();
/// android has a maximum log size so divide up the log to print everything (there's gaps in between...)
if (Platform.isAndroid && _resToString.length > 853) {
final int _splitLength = (_resToString.length / 853).ceil();
List.generate(_splitLength, (int index) {
print(_resToString.substring(index * 853, index == _splitLength - 1 ? _resToString.length : (index + 1) * 853));
});
} else {
print("response: ${_resToString}\n");
}
}
return {"success": _decodedBody["response"]};
} catch (e) {
if (e.runtimeType == SocketException) return { "error": "서버와 연결이 원활하지 않습니다. 다시 시도해주세요." };
return { "error": e };
}
}
Obviously, you would need to tune the method depending on how you receive responses from your server, but I think it can serve as a general template.
Converting Response to Class
For me, most of this happens in the “service” files. Here is an example of a SocialAccount class I’ll be using throughout this part.
class SocialAccount {
final SocialType socialType;
final String accountName;
final String socialLink;
const SNSAccount({required this.socialLink, required this.socialType, required this.accountName});
String get linkText => this.socialType == SNSType.instagram ? "@" + this.socialLink : "website";
factory SocialAccount.fromJson(Json json){
SocialType _socialType;
switch ((json["socialType"] ?? "").toString().toLowerCase()) {
case "instagram" : _snsType = SocialType.instagram;
case "youtube": _snsType = SocialType.youtube;
default : _snsType = SocialType.website;
}
return SocialAccount(
socialLink: (json["socialLink"] ?? "").toString(),
socialType: _snsType,
accountName: (json["accountName"] ?? "").toString(),
);
}
}
FYI:
- for my json, I always assume the key might be nullable due to a mistake, which is why in all my json conversion methods, I make it null-proof (eg. (json[“socialLink”] ?? “”).toString())
- Converting response to class using factory method
This portion is pretty straightforward. You just put respective json as the factory argument.
Future<Json> fetchSocialInfo() async {
final Json _res = await this._connect.reqAPIServer(path: "...", accessToken: accessToken);
if (true for successful response) {
return {"socialAccount": SocialAccount.fromJson(_res[""]) };
}
if (false for error) {
/// account for respective errors and its error messages
return {"error": _errorMsg };
}
return _res;
}
2. Converting Json list to a list of a class
After writing about 5 individual methods for converting json list to a specific class using the factory constructor, I wanted to be more efficient. For example, a user might have several social accounts (hence, a json list of social accounts and its info). Therefore, I made a method that takes a json list and the factory constructor, which returns List<T>.
static List<T> convertJsonListToClass<T>({required List<dynamic> jsonList, required T Function(Json) factory}) {
final List<Json> _jsonList = List<Json>.from(jsonList);
List<T> _list = [];
_jsonList.forEach((Json json) => _list.add(factory(json)));
return _list;
}
---------------------------------------------
class User {
List<SocialAccount> socialAccounts;
factory User.fromJson(Json json) => User(
socialAccounts: convertJsonListToClass<SocialAccount>(jsonList: json["socialAccounts"], factory: SocialAccounts.fromJson)
);
}
3. Using Isolate to convert Json list
I use the method in 2 mostly within other factories, and in most “service” files, I use an isolate to convert the json list. Although I implemented the Isolate in hopes that it would increase performance, I did not have time to actually compare the results.
static Future<List<T>?> convertJsonToClassInIsolate<T>({required List<dynamic> jsonList, required T Function(Json) factory}) async {
try {
return await Isolate.run(() {
final List<Json> _jsonList = List<Json>.from(jsonList);
List<T> _list = [];
_jsonList.forEach((Json json) => _list.add(factory(json)));
return _list;
});
} catch (e) {
if (kDebugMode|| kProfileMode) print("error - convertJsonInIsolate: ${e.toString()}");
return null;
}
}
Happy coding!