Basics

Networking



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.

  1. 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
...
  1. 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
...
  1. Use the api helper, this method is shorter and works by using your apiDecoders 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.