How to fetch network data in Flutter (using http and ScopedModel)
in this tutorial we are going to fetch Github users from Github API. However, instead of just focusing on http and placing the call in the main file, we are going to use ScopedModel. Github source is in the end of the article.
Add dependencies
- Add
http: <newer_version>
to the dependencies in yourpubspec.yaml
. - Add
scoped_model: <newer_version>
to the dependencies in yourpubspec.yaml
. - You can check the versions here: http and scoped_model.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
scoped_model: ^1.0.1
http: ^0.12.0+2
1. Build the basic structure of the app
Our app is going to be simple, it will have a single page: UsersPage
, which will display the Github users.
We will also need a User
class to store the users information and a UserModel
, where we are going to place the http call to fetch the data.
./main.dart
import 'package:flutter/material.dart';
import 'users.dart';
main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Scoped Model Http',
routes: {
'/': (context) => UsersPage(),
},
);
}
}
./users.dart
import 'package:flutter/material.dart';
class UsersPage extends StatefulWidget {
@override
_UsersPageState createState() => _UsersPageState();
}
class _UsersPageState extends State<UsersPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Github Users'),
),
body: _buildBody(),
);
}
Widget _buildBody() {
// TODO
}
}
./model/user.dart
class User {
final String login;
final int id;
final String nodeId;
final String type;
User(this.login, this.id, this.nodeId, this.type);
}
./scoped_model/user_model.dart
import 'package:scoped_model/scoped_model.dart';
class UserModel extends Model {
// TODO
}
2. Implement http call
Step 1. Write your model
UserModel
will be the one responsible to hold the list of all users and to have a method which fetch the users data.
./scoped_model/user_model.dart
import 'package:scoped_model/scoped_model.dart';
import 'package:http/http.dart' as http;
import '../model/user.dart';
class UserModel extends Model {
List<User> users = [];
void fetchUsers() async {
try {
http.Response response = await http.get('https://api.github.com/users');
if (response.statusCode == 200 || response.statusCode == 201) {
print(response.body);
}
} catch (error) {
print(error);
}
}
}
- When importing http, don't forge to put
as http
in front of the import statement, this will allow you to call the http methods by usinghttp.<some_method>
. users
: This will hold a list ofUser
.fetchUsers()
: This is an async method, it means it is going to execute the next lines only whenawait
line is done. Callhttp.get(uri)
to fetch all users data. In the end, in case the response is successfully we print the json data.
Step 2. Wrap your app into a ScopedModel
I wrote an article with more details about How to use ScopedMode in Flutter. If you do not understand this part, check it out.
./main.dart
...
import 'package:scoped_model/scoped_model.dart';
import 'users.dart';
import './scoped_model/user_model.dart';
...
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final model = UserModel();
return ScopedModel<UserModel>(
model: model,
child: _buildMaterialApp(model),
);
}
MaterialApp _buildMaterialApp(UserModel model) {
return MaterialApp(
title: 'Scoped Model Http',
routes: {
'/': (context) => UsersPage(model),
},
);
}
}
We create a UserModel model
variable so we can pass it to our _buildMaterialApp()
method and then pass it to the constructor of UsersPage
, because we are going to need an instance of our model in this page.
Step 3. Receive the model and make the call
./users.dart
...
import './scoped_model/user_model.dart';
class UsersPage extends StatefulWidget {
final UserModel model; // Receive a model
UsersPage(this.model);
@override
_UsersPageState createState() => _UsersPageState();
}
class _UsersPageState extends State<UsersPage> {
@override
void initState() {
super.initState();
widget.model.fetchUsers();
}
...
}
Here we are first just receiving a UserModel
variable in the constructor of our class.
Second, we use this model to call fetchUsers()
method we've created in the step 1.
Note that to access the model
from our State<>
we need to call widget.model
.
Run your app and test it. When you start your application UsersPage
will be created and initState()
will be called. Then, fetchUsers()
will be called and in the end of a successful response it will print the response data.
This will output the following result.
[
{
“login”:”mojombo”,
”id”:1,
”node_id”:”MDQ6VXNlcjE=”,
”avatar_url”:”...",
"gravatar_id":"",
...
"received_events_url":"...",
"type":"User",
"site_admin":false
},
{
"login":"defunkt",
"id":2,
"node_id":"MDQ6VXNlcjI=",
"avatar_url":"...",
"gravatar_i":"..."
}
]
3. Load data into a ListView
Step 1. Save the data into your List
In our user_model
we got a response from the code above. This is a List
of User
, however, the response does not know that each data in the List
is a User
. Therefore, we should suppose that our result is going to be a List
of something we don't know yet. In flutter we use dynamic
to refer to it.
./scoped_model/user_model.dart
import 'dart:convert';...void fetchUsers() async {
try {
http.Response response = await http.get('https://api.github.com/users');
if (response.statusCode == 200 || response.statusCode == 201) {
// 1. Create a List of Users
final List<User> fetchedUserList = [];
// 2. Decode the response body
List<dynamic> responseData = jsonDecode(response.body);
// 3. Iterate through all the users in the list
responseData?.forEach((dynamic userData) {
// 4. Create a new user and add to the list
final User user = User(userData['login'], userData['id'],
userData['node_id'], userData['type']);
fetchedUserList.add(user);
});
// 5. Update our list and the UI
_users = fetchedUserList;
notifyListeners();
}
} catch (error) {
print(error);
}
}
The code here is not so complex. Let's understand step by step.
- If the response is successful we end up here. We are going to create a new list of users where we are going to store each user from our response.
- Now we need to import the
dart:convert
to decode our response. Theresponse.body
will be inJson
, we will decode it into aList<dynamic>
. - We iterate through all the users in the
List<dynamic> responseData
. - We create a
User
with the data we get and add it to theList<User>
we've created in the beginning. - We save our List to the private variable we have in our scoped model:
List<User> _users
.
IMPORTANT: This call is going to take a while, it is asynchronous. When we open our UsersPage
it will call fetchUsers()
inside initState()
. This means the users will start to be fetching since the page is created. Let's suppose our call will take 5 seconds to be completed. Hence, our page will be created, and after 5 seconds the users will be displayed on the screen.
In this 5 seconds, our List<User> _users
inside our scoped model is empty yet. Then, we can not display the users. After the 5 seconds we need the scoped model to tell the UsersPage
that we've completed the call and we should rebuild the UsersPage
. This is the work of notifyListeners()
.
Step 2. Load the List into a ListView
When building the body of our UsersPage
we have to wrap it into a ScopedModelDescendant
to get an instance of UserModel
.
We will first of all check if our list of users is empty or not.
In case it is empty, it means we do not finished loading the list yet, then we display a Text
saying there is no users (we will change for a CircularProgressIndicator
latter).
In case it is not empty, we build the ListView
.
./users.dart
Widget _buildBody() {
return ScopedModelDescendant<UserModel>(
builder: (BuildContext context, Widget child, UserModel model) {
if (model.users.length > 0) { // not empty
return _buildListView(model);
} else { // empty
return Center(
child: Text('No users'),
);
}
},
);
}
Widget _buildListView(UserModel model) {
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(model.users[index].login),
subtitle: Text(model.users[index].nodeId),
);
},
itemCount: model.users.length,
);
}
To build the ListView
we will use ListView.builder
because we have lots of data and it helps us to load the data on demand. For the itemCount
we pass the length of our users list and for the itemBuilder
we just create a ListTile
with the login
and the nodeId
.
4. Display a CircularProgressIndicator
We will create a new variable inside our UserModel
: bool _isLoading
, it is going to be responsible to tell if we are loading data or if we've finished it.
./scoped_model/user_model.dart
class UserModel extends Model {
bool _isLoading = false;
...
bool get isLoading {
return _isLoading;
}
void fetchUsers() async {
_isLoading = true; // here
notifyListeners();
try {
http.Response response = await http.get('https://api.github.com/users');
if (response.statusCode == 200 || response.statusCode == 201) {
final List<User> fetchedUserList = [];
List<dynamic> responseData = jsonDecode(response.body);
responseData?.forEach((dynamic userData) {
final User user = User(userData['login'], userData['id'],
userData['node_id'], userData['type']);
fetchedUserList.add(user);
});
_users = fetchedUserList;
}
_isLoading = false; // here
notifyListeners();
} catch (error) {
print(error);
_isLoading = false; // here
notifyListeners();
}
}
}
Whenever we update _isLoading
variable we call notifyListeners()
to tell our page we've changed something.
We set _isLoading
to true
in the beginning of fetchUsers()
method to tell we are loading data. When data is loaded or if we end up in some error we set _isLoading
to false to tell we've finished loading data.
./users.dart
Widget _buildBody() {
return ScopedModelDescendant<UserModel>(
builder: (BuildContext context, Widget child, UserModel model) {
Widget content = Text('No users');
if (model.isLoading) {
content = Center(
child: CircularProgressIndicator(),
);
} else if (!model.isLoading && model.users.length > 0) {
content = _buildListView(model);
}
return content;
},
);
}
Here we made small changes. We have by default a Text saying we have no users, then we check if we are loading or not.
If we are loading, display a loading indicator.
If we are not loading and there is users, we display the ListView
with the users.
If there is no users, we display the default No Users text.
NOTE: If your project is working weirdly, try to close the app and rerun it. It will probably not work on hot reload.