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.

Data

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.

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"

}

}

Installation

To install Dio package, we need go to file pubspec.yaml inside 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.

Model

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>.fromJson which 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 .

API Provider

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

Repository

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

}

}

Bloc

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

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

Loading Error 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 builder parameter 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 .

Beautify UI

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