In this tutorial I show you how to set up a basic flutter app that communicates with a REST API.
REST APIs are a common way to implement communication between an app and a server.
In this tutorial, I show you how I structure my Flutter apps in a simple, yet scalable way to retrieve and send
data from/to a server and display it in the app.
We will use MVVM (Model-View-ViewModel) as the foundation of our app.
The model describes the data that we are using and should be displayed in our app.
In our example it will be users that have a name, email address and a avatar.
The view refers to the UI that displays the data. In Flutter these are our widgets.
The view model contains the logic of our app. For this tutorial, this will be a riverpod Notifier that holds and
handles state changes.
Additionally, we will implement a repository layer (or data layer) that is responsible for fetching data for a
source. We are going to add abstraction here to easily switch between different sources.
Required Packages
For this tutorial, I will add the following packages to my app:
riverpod: a powerful state management and dependency injection solution
freezed: generates utility functions for data classes
We use this information to derive a User class that contains this data.
Using freezed and json_serializable we generate useful functions such as copyWith and toJson;
Let’s start the build_runner to generate the required files.
dart run build_runner watch --delete-conflicting-outputs
The Repository Layer
Next, we define our repository layer. Here we will create an abstraction for being able to switch between
different data
sources as needed.
In this example, we will create an interface UserRepository defining the functions of the repository and
create two concrete implementations:
MockUserRepository is going to be a mock implementation. This can be used to quickly test your implementation,
or
start working even though the API might not be yet ready to be used.
ApiUserRepository is going to be the actual implementation using the API Client generated with retrofit.
The UserRepository interface
We use the interface class to define which functions the implementations should have.
To keep this tutorial short, we will focus on a single function:
Before actually implementing API calls, let’s create a mock repository first.
This way we can start working on the rest of the app before actually needing access to the API.
In many projects, APIs might not be available in the early stages of the project, this way you can still start working.
import'package:src/user/model/user.dart';import'package:src/user/repository/user_repository.dart';classMockUserRepositoryimplementsUserRepository{staticconst_mockUsers=[User(id:1,email:'Tim@testmail.com',firstName:'Tim',lastName:'Tester',avatar:'https://reqres.in/img/faces/1-image.jpg',),User(id:2,email:'toby@testmail.com',firstName:'Toby',lastName:'Tester',avatar:'https://reqres.in/img/faces/2-image.jpg',),];@overrideFuture<List<User>>getAllUsers()async{// add delay to mimic an api call
awaitFuture.delayed(constDuration(milliseconds:300));return_mockUsers;}}
Back in the file of the UserRepository, create a riverpod provider returning the MockUserRepository.
As you can see, the implementation is quite simple.
In line 11 we use the riverpod ref.watch function to retrieve our user repository instance.
Since we defined it to be a MockUserRepository we will be getting mock users from the repository.
Notice that the controller itself has no knowledge which implementation is used. We can later swap the mock
implementation with an api implementation without needing to touch the controller again.
The UI
Let’s create new page for our application and name it UserListPage.
Extending the ConsumerWidget class gives us access to riverpod’s WidgetRef that we use to watch the
UserController state.
We handle loading and error state and render the list of users and some of their attributes.s
To make riverpod work, we need to wrap our App widget with a ProviderScope.
Finally, we set the UserListPage as the home attribute of the MaterialApp widget.
{"page":1,"per_page":6,"total":12,"total_pages":2,"data":[{"id":1,"email":"george.bluth@reqres.in","first_name":"George","last_name":"Bluth","avatar":"https://reqres.in/img/faces/1-image.jpg"},{"id":2,"email":"janet.weaver@reqres.in","first_name":"Janet","last_name":"Weaver","avatar":"https://reqres.in/img/faces/2-image.jpg"},{"id":3,"email":"emma.wong@reqres.in","first_name":"Emma","last_name":"Wong","avatar":"https://reqres.in/img/faces/3-image.jpg"},{"id":4,"email":"eve.holt@reqres.in","first_name":"Eve","last_name":"Holt","avatar":"https://reqres.in/img/faces/4-image.jpg"},{"id":5,"email":"charles.morris@reqres.in","first_name":"Charles","last_name":"Morris","avatar":"https://reqres.in/img/faces/5-image.jpg"},{"id":6,"email":"tracey.ramos@reqres.in","first_name":"Tracey","last_name":"Ramos","avatar":"https://reqres.in/img/faces/6-image.jpg"}],"support":{"url":"https://reqres.in/#support-heading","text":"To keep ReqRes free, contributions towards server costs are appreciated!"}}
We can see that the data attribute contains a list of User objects that we created earlier.
To correctly parse the response, we create another model called UserListResponse.
To simplify the model, we ignore all attributes that we don’t use in the app.
import'dart:developer';import'package:src/user/client/user_api_client.dart';import'package:src/user/model/user.dart';import'package:src/user/repository/user_repository.dart';classApiUserRepositoryimplementsUserRepository{finalUserApiClientclient;constApiUserRepository(this.client);@overrideFuture<List<User>>getAllUsers()async{try{finalresponse=awaitclient.getUserList();returnresponse.data;}catch(error,trace){log('could not fetch users',error:error,stackTrace:trace);rethrow;}}}
As a last step, we need to replace the MockUserRepository with the newly created ApiUserRepository in the provider.