Flutter HTTP requests with Dio, RxDart and Bloc

Photo by Mike Aunzo on Unsplash

Making HTTP requests in mobile application is one of the common tasks. Thanks to http requests, application can communicate with backend and selects data.

Flutter framework offers http package which works great when we need do basic stuff. When we need to do something more advanced, we need something bigger. And this can be done by using Dio. Dio is http connection library which has extra features like interceptors that will be helpful in many tasks (adding token authentication for each request, logging requests). Dio API is pretty easy and the library is being maintained by the authors. It’s really worth trying.

Todays modern mobile development hot topic is reactive paradigm. Almost every application uses reactive paradigm, which is great. RxDart package offers support for basic reactive things just like Subjects or Observables. For our project it will more than enough.

Managing widget/application state is open topic in Flutter. There are many implementations like Bloc and Redux(2020 update: Provider is also worth mentioning here. Check whole list here). In this article we will use Bloc pattern which is pretty simple and powerful. You can use whatever you want in your project.

In this article i’m going to show you how to work with Dio, RxDart and Bloc to create basic application which loads data from external resource and show it in application.

We’re going to use random user API which generates fake user data and returns it in JSON format. For that case we will use http://randomuser.me API. The random user service accepts request on this endpoint:

Want to read this story later? Save it in Journal.

GET https://randomuser.me/api/

The result of this endpoint is this JSON:

{  
"results":[
{
"gender":"male",
"name":{
"title":"mr",
"first":"william",
"last":"brar"
},
"location":{
"street":"8335 stanley way",
"city":"lloydminster",
"state":"northwest territories",
"postcode":"Q4D 8S9",
"coordinates":{
"latitude":"-62.1569",
"longitude":"-79.5201"
},
"timezone":{
"offset":"-11:00",
"description":"Midway Island, Samoa"
}
},
"email":"william.brar@example.com",
"login":{
"uuid":"f10dd86d-3297-4e4c-a33d-65d009740057",
"username":"brownsnake466",
"password":"citroen",
"salt":"ojLC7qVQ",
"md5":"8f30c2d6382c9290c979cf585b15cca3",
"sha1":"57ded975628df8205d1d9b6eb11b0ffa7baafa43",
"sha256":"b8f87131af64ee9c315411dadb499c8d10bdf00985bcd4b4a52c32b76ba84f9c"
},
"dob":{
"date":"1961-07-24T23:24:35Z",
"age":57
},
"registered":{
"date":"2015-06-04T07:15:15Z",
"age":3
},
"phone":"064-200-3481",
"cell":"790-556-8085",
"id":{
"name":"",
"value":null
},
"picture":{
"large":"https://randomuser.me/api/portraits/men/1.jpg",
"medium":"https://randomuser.me/api/portraits/med/men/1.jpg",
"thumbnail":"https://randomuser.me/api/portraits/thumb/men/1.jpg"
},
"nat":"CA"
}
],
"info":{
"seed":"7ea59fb8e50ce24f",
"results":1,
"page":1,
"version":"1.2"
}
}

To install Dio package, we need go to file pubspec.yamlinside Flutter project and add this line:

dio: ^3.0.8

^3.0.8 notation means that we are accepting 3.0.x versions of Dio, where x≥8 .

(You can check current Dio version here: https://pub.dartlang.org/packages/dio)

In our project we also need RxDart, so let’s add it:

rxdart: ^0.23.1

Now we are ready to run flutter packages get command. This command downloads packages and enable them in project.

First step is model. We need to create class structures which correspond to JSON response from API. We don’t need to map all the data from API, since it won’t be useful. Let’s create these classes:

class Location {
final String street;
final String city;
final String state;


Location(this.street, this.city, this.state);

Location.fromJson(Map<String, dynamic> json)
: street = json["street"],
city = json["city"],
state = json["state"];
}
class Name {
final String title;
final String first;
final String last;

Name(this.title, this.first, this.last);

Name.fromJson(Map<String, dynamic> json)
: title = json["title"],
first = json["first"],
last = json["last"];
}
class Picture {
final String large;
final String medium;
final String thumbnail;

Picture(this.large, this.medium, this.thumbnail);

Picture.fromJson(Map<String, dynamic> json)
: large = json["large"],
medium = json["medium"],
thumbnail = json["thumbnail"];
}
import 'package:user/model/location.dart';
import 'package:user/model/name.dart';
import 'package:user/model/picture.dart';

class User {
final String gender;
final Name name;
final Location location;
final String email;
final Picture picture;

User(this.gender, this.name, this.location, this.email, this.picture);

User.fromJson(Map<String, dynamic> json)
: gender = json["gender"],
name = Name.fromJson(json["name"]),
location = Location.fromJson(json["location"]),
email = json["email"],
picture = Picture.fromJson(json["picture"]);

}
import 'package:user/model/user.dart';

class UserResponse {
final List<User> results;
final String error;

UserResponse(this.results, this.error);

UserResponse.fromJson(Map<String, dynamic> json)
: results =
(json["results"] as List).map((i) => new User.fromJson(i)).toList(),
error = "";

UserResponse.withError(String errorValue)
: results = List(),
error = errorValue;
}

Each class contains final fields and constructors (final fields must be initiated in construction part). You can find special named constructor <class>.fromJsonwhich constructs class from Map<String,dynamic>. This map will be created by Dio from endpoint response. When we init list field in UserResponse we need to setup it more complex way, which includes projection to List and map function which maps each row to User class. The UserResponse class has additional parameter error which is not being returned from API. This field will be helpful when we need to store information about any error that happend in connection process. Because of this, we need to add additional named constructor which handles the error situation and this constructor is UserResponse.withError .

Since we have our model ready, we can create code which connects to endpoint and gets response.

import 'package:user/model/user_response.dart';
import 'package:dio/dio.dart';

class UserApiProvider{
final String _endpoint = "https://randomuser.me/api/";
final Dio _dio = Dio();

Future<UserResponse> getUser() async {
try {
Response response = await _dio.get(_endpoint);
return UserResponse.fromJson(response.data);
} catch (error, stacktrace) {
print("Exception occured: $error stackTrace: $stacktrace");
return UserResponse.withError("$error");
}
}
}

UserApiProvider class contains only one method getUser which connects to endpoints and returns UserResponse . The method is asynchronous, thus the return is Future<UserResponse> .

The repository class will mediate between high level components of our architecture (like bloc-s) and UserApiProvider . The UserRepository class will be repository for our random user selected from API.

import 'package:user/model/user_response.dart';
import 'package:user/repository/user_api_provider.dart';

class UserRepository{
UserApiProvider _apiProvider = UserApiProvider();

Future<UserResponse> getUser(){
return _apiProvider.getUser();
}
}

Let’s add high level component of our architecture which is bloc (business logic component — read about it here).

UserBloc is the only component which can be used from UI class (in terms of clean architecture). UserBloc fetches data from repository and publish it via Rx subjects.

import 'package:user/model/user_response.dart';
import 'package:user/repository/user_repository.dart';
import 'package:rxdart/rxdart.dart';

class UserBloc {
final UserRepository _repository = UserRepository();
final BehaviorSubject<UserResponse> _subject =
BehaviorSubject<UserResponse>();

getUser() async {
UserResponse response = await _repository.getUser();
_subject.sink.add(response);
}

dispose() {
_subject.close();
}

BehaviorSubject<UserResponse> get subject => _subject;

}
final bloc = UserBloc();

getUser method gets data from repository and publish them in _subject .

BehaviorSubject is subject which returns last emitted value when new observer joins. This can be helpful when our widget will change his state. The observer will be our widget which will show user data.

dispose method should be called, when UserBloc will be no longer used.

Widget which will display user data will be build around StreamBuilder component. There will be 3 states:

  1. Loading
  2. Error
  3. Success

Loading state is default one. It will shows progress indicator and loading text. Error state can happen when connection with API fails (for example when user goes offline). Success is state when data was loaded sucessfully.

StreamBuilder has 2 important parameters: stream which is our source of data (the component will change his state when something new has pushed through stream) and builder which allows to create child widget based on current state.

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:user/bloc/user_bloc.dart';
import 'package:user/model/user_response.dart';

class UserWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _UserWidgetState();
}
}

class _UserWidgetState extends State<UserWidget> {

@override
void initState() {
super.initState();
bloc.getUser();
}

@override
Widget build(BuildContext context) {
return StreamBuilder<UserResponse>(
stream: bloc.subject.stream,
builder: (context, AsyncSnapshot<UserResponse> snapshot) {
if (snapshot.hasData) {
if (snapshot.data.error != null && snapshot.data.error.length > 0){
return _buildErrorWidget(snapshot.data.error);
}
return _buildUserWidget(snapshot.data);

} else if (snapshot.hasError) {
return _buildErrorWidget(snapshot.error);
} else {
return _buildLoadingWidget();
}
},
);
}

Widget _buildLoadingWidget() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [Text("Loading data from API..."), CircularProgressIndicator()],
));
}

Widget _buildErrorWidget(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Error occured: $error"),
],
));
}

Widget _buildUserWidget(UserResponse data) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("User widget"),
],
));
}
}

_UserWidgetState is class which implements UserWidget state. In initState we inform bloc that is time to load data. In Streambuilder’s builderparameter we decide what should be displayed at this moment. When there is no data yet, we will show loading widget which is build by _buildLoadingWidget method. Once error occured, we’ll show error widget build by _buildErrorWidget method and when the data was sucessfully returned, we’ll use _buildUserWidget .

Our mockup from previous point shows only Text widget. It’s time to build something more user friendly.

Firstly, there was added background gradient. This was done by wrapping UserWidget with Container widget which has BoxDecoration with LinearGradient .

Container(
child: UserWidget(),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: [0.0, 0.7],
colors: [
Color(0xFFF12711),
Color(0xFFf5af19),
],
),
),
)

Secondly, we will declare ThemeData which contains Text styles.

MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primaryColor: Colors.white,
textTheme: TextTheme(
title: TextStyle(fontSize: 30, color: Colors.white),
subtitle: TextStyle(fontSize: 20, color: Colors.white),
body1: TextStyle(fontSize: 15, color: Colors.white)),
),
home: MyHomePage(),
);

Declaring theme data keeps our code cleaner, because we don’t need to specify in each Text component style data.

Next, it’s time to modify loading, error and success widgets. Loading and error widgets has received style parameter:

Text("Loading data from API...",
style: Theme.of(context).textTheme.subtitle),

Success widget has been modified much more. It has now CircleAvatar and multiple Text widgets which display user data. Please note that between Text components we’ve added Padding widgets to create space in layout.

Widget _buildUserWidget(UserResponse data) {
User user = data.results[0];
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
radius: 70,
backgroundImage: NetworkImage(user.picture.large),
),
Text(
"${_capitalizeFirstLetter(user.name.first)} ${_capitalizeFirstLetter(user.name.last)}",
style: Theme.of(context).textTheme.title,
),
Text(user.email, style: Theme.of(context).textTheme.subtitle),
Padding(
padding: EdgeInsets.only(top: 5),
),
Text(user.location.street, style: Theme.of(context).textTheme.body1),
Padding(
padding: EdgeInsets.only(top: 5),
),
Text(user.location.city, style: Theme.of(context).textTheme.body1),
Padding(
padding: EdgeInsets.only(top: 5),
),
Text(user.location.state, style: Theme.of(context).textTheme.body1),
],
));
}

When we’re reaching external resources like API, many problems may happend. One of the most common error is SocketException which happens when we’re offline. In our code we have try catch block, and our application will be not killed when this error occurs. Unfortunately, user will see this error in his screen:

Exception occured: DioError [DioErrorType.DEFAULT]: SocketException: Failed host lookup: ‘randomuser.me’ (OS Error: No address associated with hostname, errno = 7)

Lets create _handleError method which will translate DioError into human readable message.

String _handleError(Error error) {
String errorDescription = "";
if (error is DioError) {
DioError dioError = error as DioError;
switch (dioError.type) {
case DioErrorType.CANCEL:
errorDescription = "Request to API server was cancelled";
break;
case DioErrorType.CONNECT_TIMEOUT:
errorDescription = "Connection timeout with API server";
break;
case DioErrorType.DEFAULT:
errorDescription =
"Connection to API server failed due to internet connection";
break;
case DioErrorType.RECEIVE_TIMEOUT:
errorDescription = "Receive timeout in connection with API server";
break;
case DioErrorType.RESPONSE:
errorDescription =
"Received invalid status code: ${dioError.response.statusCode}";
break;
case DioErrorType.SEND_TIMEOUT:
errorDescription = "Send timeout in connection with API server";
break;
}
} else {
errorDescription = "Unexpected error occured";
}
return errorDescription;
}
}

Dio providers multiple error types which can be handled by us. DioError has response and requestobject, which can be used to show for example invalid status code.

Then we can use _handleError method in catch block:

...
} catch (error, stacktrace) {
print("Exception occured: $error stackTrace: $stacktrace");
return UserResponse.withError(_handleError(error));
}

When we want change Dio default behavior just like connection timeout time we can use Options in Dio constructor.

Dio _dio;

UserApiProvider() {
BaseOptions options =
BaseOptions(receiveTimeout: 5000, connectTimeout: 5000);
_dio = Dio(options);
}

We have added default constructor for UserApiProvider class. In this constructor we need to create BaseOptions instance and specify parameters that we want to change. To see all parameters that can be changed, click here. When our BaseOptions instance is ready, we can pass it to Dio constructor.

One of the common problem is adding some behavior for each request/response that is being made. We can add code for each method in API provider classes, but this will be useless since Dio provides interceptors.

Let’s create interceptor which logs each response/request just like OkHttp logging interceptor style.

Dio instance has interceptors field where we can add our log interceptor. Code from our interceptor will be called before request and after response.

class LoggingInterceptor extends Interceptor{

int _maxCharactersPerLine = 200;

@override
Future onRequest(RequestOptions options) {
print("--> ${options.method} ${options.path}");
print("Content type: ${options.contentType}");
print("<-- END HTTP");
return super.onRequest(options);
}

@override
Future onResponse(Response response) {
print(
"<-- ${response.statusCode} ${response.request.method} ${response.request.path}");
String responseAsString = response.data.toString();
if (responseAsString.length > _maxCharactersPerLine) {
int iterations =
(responseAsString.length / _maxCharactersPerLine).floor();
for (int i = 0; i <= iterations; i++) {
int endingIndex = i * _maxCharactersPerLine + _maxCharactersPerLine;
if (endingIndex > responseAsString.length) {
endingIndex = responseAsString.length;
}
print(responseAsString.substring(
i * _maxCharactersPerLine, endingIndex));
}
} else {
print(response.data);
}
print("<-- END HTTP");

return super.onResponse(response);
}

@override
Future onError(DioError err) {
print("<-- Error -->");
print(err.error);
print(err.message);
return super.onError(err);
}

}

Here is our LoggingInterceptor class. It’s extends Interceptor class provided by Dio. We need to override onRequest , onRespone and onError . We are adding our behaviour (which is just print to console) in these methods.

Very often returned response is very long and doesn’t fit console view. We need to split response longer than maxCharacterPerLine into multiple lines, so it will fit inside console. Our logger will print response output in multiple lines.

Final step: let’s add our interceptor to Dio:

_dio.interceptors.add(LoggingInterceptor());

After application restart, you should see logs in console:

That’s all for the basic stuff. You can check code for this project here:

Feel free to visit my open source weather application build in Flutter:

Thanks for reading!

Jan 2020: Revisited article and updated stuff!

📝 Read this story later in Journal.

👩‍💻 Wake up every Sunday morning to the week’s most noteworthy stories in Tech waiting in your inbox. Read the Noteworthy in Tech newsletter.

Software developer @ Better Software Group