Networking
- Introduction
- Making HTTP requests
- Using an API Service
- Create an API Service
- Morphing JSON payloads to models
- Retrying failed requests
- Refreshing tokens
- Singleton API Service
- Caching Responses
Introduction
Nylo makes networking in modern mobile applications simple. You can do GET, PUT, POST and DELETE requests via the base networking class.
Your API Services directory is located here app/networking/*
Fresh copies of Nylo will include a default API Service app/networking/api_service.dart
.
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(buildContext);
@override
String get baseUrl => "https://jsonplaceholder.typicode.com";
@override
get interceptors => {
LoggingInterceptor: LoggingInterceptor()
};
Future<User?> fetchUsers() async {
return await network<User>(
request: (request) => request.get("/users"),
);
}
}
Variables you can override using the NyApiService class.
-
baseUrl
- This is the base URL for the API, e.g. "https://jsonplaceholder.typicode.com". -
interceptors
- Here, you can add Dio interceptors. Learn more about interceptors here. -
useInterceptors
- You can set this to true or false. It will let the API Service know whether to use your interceptors.
Under the hood, the base networking class uses Dio, a powerful HTTP client.
Making HTTP requests
In your API Service, use the network
method to build your API request.
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(buildContext, decoders: modelDecoders);
@override
String get baseUrl => "https://jsonplaceholder.typicode.com";
Future<dynamic> fetchUsers() async {
return await network(
request: (request) {
// [GET] return request.get("/users");
// [PUT] return request.put("/users", data: {"user": "data"});
// [POST] return request.post("/users", data: {"user": "data"});
// [DELETE] return request.delete("/users/1");
return request.get("/users");
},
);
}
The request
argument is a Dio instance, so you can call all the methods from that object.
Base Options
The BaseOptions
variable is highly configurable to allow you to modify how your Api Service should send your requests.
Inside your API Service, you can use the baseOptions
parameter to modify the BaseOptions
variable.
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(
buildContext,
decoders: modelDecoders,
baseOptions: (BaseOptions baseOptions) {
return baseOptions
..connectTimeout = Duration(seconds: 5)
..sendTimeout = Duration(seconds: 5)
..receiveTimeout = Duration(seconds: 5);
}
...
Click here to view all the base options you can set.
Adding Headers
You can add headers to your requests either via your baseOptions variable, on each request or an interceptor.
Here's the simplest way to add headers to a request.
Future fetchWithHeaders() async => await network(
request: (request) => request.get("/test"),
headers: {
"Authorization": "Bearer aToken123",
"Device": "iPhone"
}
);
You can also add Bearer Token's like in the below example.
Future fetchUserWithBearer() async => await network(
request: (request) => request.get("/user"),
bearerToken: "hello-world-123",
);
Or lastly, like the below.
...
Future fetchUsers() async {
return await network(
request: (request) {
request.options.headers.addAll({
"Authorization": "Bearer $token"
});
return request.get("/users");
},
);
}
Interceptors
If you're new to interceptors, don't worry. They're a new concept for managing how your HTTP requests are sent.
Put in simple terms. An 'interceptor' will intercept the request, allowing you to modify the request before it's sent, handle the response after it completes and also what happens if there's an error.
Nylo allows you to add new interceptors to your API Services like in the below example.
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(buildContext, decoders: modelDecoders);
...
@override
Map<Type, Interceptor> get interceptors => {
LoggingInterceptor: LoggingInterceptor(),
// Add more interceptors for the API Service
// BearerAuthInterceptor: BearerAuthInterceptor(),
};
...
Let's take a look at an interceptor.
Example of a custom interceptor.
import 'package:nylo_framework/nylo_framework.dart';
class CustomInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// options - modify the RequestOptions before the request
return super.onRequest(options, handler);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
// response - handle/manipulate the Response object
handler.next(response);
}
@override
void onError(DioError error, ErrorInterceptorHandler handler) {
// error - handle the DioError object
handler.next(err);
}
}
Fresh copies on Nylo will include a app/networking/dio/intecetors/*
directory.
Creating a new interceptor
You can create a new interceptor using the command below.
# Run this command in your terminal
dart run nylo_framework:main make:interceptor logging_interceptor
File: app/networking/dio/intecetors/logging_interceptor.dart
import 'dart:developer';
import 'package:nylo_framework/nylo_framework.dart';
class LoggingInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
print('REQUEST[${options.method}] => PATH: ${options.path}');
return super.onRequest(options, handler);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
print(
'RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions
.path}');
print('DATA: ${response.requestOptions.path}');
log(response.data.toString());
handler.next(response);
}
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
print('ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions
.path}');
handler.next(err);
}
}
Understanding the network helper
The network
helper provides us with a way to make HTTP requests from our application.
The helper method can be accessed when using an API Service in Nylo.
class ApiService extends NyApiService {
...
Future<dynamic> fetchUsers() async {
return await network(
request: (request) {
// [GET] return request.get("/users");
// [PUT] return request.put("/users", data: {"user": "data"});
// [POST] return request.post("/users", data: {"user": "data"});
// [DELETE] return request.delete("/users/1");
return request.get("/users");
},
);
}
Return Types
There are two ways to handle the response from an HTTP request. Let's take a look at both in action, there's no right or wrong way to do this.
Using Model Decoders
Model Decoders are a new concept introduced in Nylo v3.x.
They make it easy to return your objects, like in the below example.
class ApiService extends NyApiService {
...
Future<User?> fetchUser() async {
return await network<User>(
request: (request) => request.get("/users/1"),
);
}
File: config/decoders.dart
final modelDecoders = {
User: (data) => User.fromJson(data), // add your model and handle the return of the object
// ...
};
The data
parameter will contain the HTTP response body.
Learn more about decoders here
Using handleSuccess
The handleSuccess: (Response response) {}
argument can be used to return a value from the HTTP body.
This method is only called if the HTTP response has a status code equal to 200.
Here's an example below.
class ApiService extends NyApiService {
...
// Example: returning an Object
Future<User?> findUser() async {
return await network(
request: (request) => request.get("/users/1"),
handleSuccess: (Response response) { // response - Dio Response object
dynamic data = response.data;
return User.fromJson(data);
}
);
}
// Example: returning a String
Future<String?> findMessage() async {
return await network(
request: (request) => request.get("/message/1"),
handleSuccess: (Response response) { // response - Dio Response object
dynamic data = response.data;
if (data['name'] == 'Anthony') {
return "It's Anthony";
}
return "Hello world";
}
);
}
// Example: returning a bool
Future<bool?> updateUser() async {
return await network(
request: (request) => request.put("/user/1", data: {"name": "Anthony"}),
handleSuccess: (Response response) { // response - Dio Response object
dynamic data = response.data;
if (data['status'] == 'OK') {
return true;
}
return false;
}
);
}
Using handleFailure
The handleFailure
method will be called if the HTTP response returns a status code not equal to 200.
You can provide the network helper with the handleFailure: (DioError dioError) {}
argument and then handle the response in the function.
Here's an example of how it works.
class ApiService extends NyApiService {
...
// Example: returning an Object
Future<User?> findUser() async {
return await network(
request: (request) => request.get("/users/1"),
handleFailure: (DioError dioError) { // response - DioError object
dynamic data = response.data;
// Handle the response
return null;
}
);
}
}
Using an API Service
When you need to call an API from a widget, there are a few approaches in Nylo, here's the most common ways.
- You can create a new instance of the API Service and then call the method you want to use, like in the below example.
class _MyHomePageState extends NyPage<MyHomePage> {
ApiService _apiService = ApiService();
@override
get init => () async {
List<User>? users = await _apiService.fetchUsers();
print(users); // List<User>? instance
...
- Adding the
HasApiService
mixin to your widget class.
class _MyHomePageState extends NyPage<MyHomePage> with HasApiService<ApiService> {
@override
get init => () async {
onApiSuccess((response, data) {
// (Optional) Handle the response
// response - Dio Response object
// data - the ApiService response data
});
onApiError((dioError) {
// (Optional) Handle the error
});
List<User>? users = await apiService.fetchUsers();
print(users); // List<User>? instance
...
- Use the
api
helper, this method is shorter and works by using yourapiDecoders
variable inside config/decoders.dart. Learn more about decoders here.
class _MyHomePageState extends NyPage<MyHomePage> {
@override
get init => () async {
User? user = await api<ApiService>((request) => request.fetchUser());
print(user); // User? instance
// or
await api<ApiService>(
(request) => request.fetchUser(),
onSuccess: (response, data) {
// data - User? instance
},
onError: (DioError dioError) {
// Handle the error
}
);
...
Using the api helper also allows you to handle UI feedback to your users if the request isn't successful.
// Your Widget
@override
Widget build(BuildContext context) {
return Scaffold(
body: MaterialButton(
onPressed: () {
User user = User();
_sendFriendRequest(user);
},
child: Text("Send Friend Request"),
);
);
}
_sendFriendRequest(User user) async {
bool? successful = await api<ApiService>(
(request) => request.sendFriendRequest(user),
onError: (DioError dioError) {
showToastOpps(description: 'Failed to send friend request');
},
onSuccess: (response, data) {
showToastSuccess(description: "Friend request sent");
}
);
}
Create an API Service
To create more api_services, e.g. a user_api_service
, use the command below.
dart run nylo_framework:main make:api_service user
You can also create an API Service for a model with the --model="User"
option.
dart run nylo_framework:main make:api_service user --model="User"
This will create an API Service with the following methods.
class UserApiService extends NyApiService {
...
/// Return a list of users
Future<List<User>?> fetchAll({dynamic query}) async {
return await network<List<User>>(
request: (request) => request.get("/endpoint-path", queryParameters: query),
);
}
/// Find a User
Future<User?> find({required int id}) async {
return await network<User>(
request: (request) => request.get("/endpoint-path/$id"),
);
}
/// Create a User
Future<User?> create({required dynamic data}) async {
return await network<User>(
request: (request) => request.post("/endpoint-path", data: data),
);
}
/// Update a User
Future<User?> update({dynamic query}) async {
return await network<User>(
request: (request) => request.put("/endpoint-path", queryParameters: query),
);
}
/// Delete a User
Future<bool?> delete({required int id}) async {
return await network<bool>(
request: (request) => request.delete("/endpoint-path/$id"),
);
}
}
Morphing JSON payloads to models
You can automatically decode JSON payloads from your API requests to models using decoders.
Here is an API request that uses Nylo's implementation of decoders.
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(buildContext, decoders: modelDecoders);
Future<User?> fetchUsers() async {
return await network<User>(
request: (request) => request.get("/users"),
);
}
}
The fetchUsers
method will handle the JSON conversion to the model representation using generics.
You will first need to add your model to your config/decoders.dart
file like the below.
/// file 'config/decoders.dart'
final modelDecoders = {
List<User>: (data) => List.from(data).map((json) => User.fromJson(json)).toList(),
User: (data) => User.fromJson(data),
// ...
};
Here you can provide the different representations of the Model, e.g. to object or list<Model> like the above.
The data argument in the decoder will contain the body payload from the API request.
To get started with decoders, check out this section of the documentation.
Retrying failed requests
In Nylo v6.x, you can retry failed requests.
If the response from the API request is an error status code (e.g. status code 500), the request will be retried.
Retrying an API request
You can retry failed requests using the retry
method.
Future fetchUsers() async {
return await network(
request: (request) => request.get("/users"),
retry: 3, // retry 3 times
);
}
Retry delay
You can also set a delay between each retry attempt.
Future fetchUsers() async {
return await network(
request: (request) => request.get("/users"),
retry: 3, // retry 3 times
retryDelay: Duration(seconds: 2), // retry after 2 second
);
}
Retry if
You can also set a condition for when to retry the request.
Future fetchUsers() async {
return await network(
request: (request) => request.get("/users"),
retry: 3, // retry 3 times
retryIf: (DioException dioException) {
// retry if the status code is 500
return dioException.response?.statusCode == 500;
},
);
}
Refreshing tokens
If your application needs to refresh tokens, you can handle this in your API Service.
In Nylo, you can override 3 methods in your API Service to handle token refreshing.
-
refreshToken
- This method will be called when the API Service needs to refresh the token. -
setAuthHeaders
- This method will be called before every request. You can add your auth headers here. -
shouldRefreshToken
- This method will be called before every request. You can return true or false to determine whether to refresh the token.
Let's take a look at all three in action.
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) :
super(buildContext, decoders: modelDecoders);
Future getUser() async {
return await network(
request: (request) => request.get("/user")
);
}
@override
refreshToken(Dio dio) async {
dynamic response = (await dio.get("https://example.com/refresh-token")).data;
// Save the new token to local storage
User user = Auth.user();
user.token = Token.fromJson(response['token']);
await user.save();
}
@override
Future<RequestHeaders> setAuthHeaders(RequestHeaders headers) async {
User? user = Auth.user();
if (user != null) {
headers.addBearerToken( user.token );
}
return headers;
}
@override
Future<bool> shouldRefreshToken() async {
User? user = Auth.user();
if (user.token.expiredAt.isPast()) {
// Check if the token is expired
// This will trigger the refreshToken method
return true;
}
return false;
}
Now when you call the getUser
method, the API Service will check if the token is expired and then refresh it if needed.
Singleton API Service
You can create an API Service as a singleton by updating the apiDecoders variable in your config/decoders.dart
file.
final Map<Type, dynamic> apiDecoders = {
ApiService: () => ApiService(), // from this
ApiService: ApiService(), // to this
...
};
Now when you call the api
helper, it will return the same instance of the API Service.
api<ApiService>((request) => request.fetchUsers());
You can switch between singleton and non-singleton API Services if you need to.
Caching Responses
In Nylo, you can cache responses from your API requests.
Caching responses can help improve the performance of your application by reducing the number of requests made to the server.
Let's imagine you have a method that fetches the countries from an API.
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(buildContext, decoders: modelDecoders);
Future<List<Country>> fetchCountries() async {
return await network<List<Country>>(
request: (request) => request.get("/countries"),
cacheKey: "app_countries", // cache key
cacheDuration: const Duration(hours: 1), // cache for 1 hour
) ?? [];
}
}
In the above example, the fetchCountries
method will cache the response for 1 hour using the cache key app_countries. Now, any subsequent calls to the fetchCountries
method will return the cached response if it's still valid.
After 1 hour has passed, Nylo will fetch the countries from the API again and cache the response for another hour.
Clearing the cache
You can also clear the cache for a specific key using the cache().clear("my_key")
method.
await cache().clear("app_countries");
Caching with the api
helper
You can also cache responses when using the api
helper.
api<ApiService>(
(request) => request.fetchCountries(),
cacheKey: "app_countries",
cacheDuration: const Duration(hours: 1),
);
Caching with the network
helper
You can also cache responses when using the network
helper.
Future<List<Country>> fetchCountries() async {
return await network<List<Country>>(
request: (request) => request.get("/countries"),
cacheKey: "app_countries",
cacheDuration: const Duration(hours: 1),
) ?? [];
}
Caching is a powerful feature that can help improve the performance of your application. Use it wisely.