Tips for Flutter: HTTP, Factory, List

Soo Kim
5 min readDec 11, 2023
https://www.freecodecamp.org/news/what-is-http/

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())

  1. 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!

--

--