Flutter and scoped_model

I’m currently learning state management with Flutter so I tried applying one of the architecture called scoped_model and made a simple app that list all contacts, and provide the details once you clicked on one of the contact. We won’t create the contacts manually. Instead, we will be using randomuser.me API to get the list of contacts.

Firstly, create the project using flutter create or project creation wizard if you are using IntelliJ IDEA or Android Studio. Then add scoped_model and http packages into your pubspec.yaml file.

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

Then save the file, and run flutter packages get if your IDE or editor did not run this automatically for you. Now create four folders inside lib folder, namely models, services, screens, and scoped_models. We need to store our model classes into models folder, our API fetcher service into services folder, our actual scoped model classes which will connect our class model and API service to our views into scoped_models folder, and all of our UI into views folder.

Once it’s done, create a file inside models folder and name it contact.dart since we need a class model for the API. You can construct that class yourself but to speed things up I’m using JSON to Dart generator to automatically convert JSON format to proper Dart class model format. We are going to only request for 20 users or contacts so use this link and convert using the generator to get the model class. You can remove all the to_json methods since we aren’t using those. The class somewhat looks like this:

// lib/models/contact.dart

class Contact {
  String gender;
  Name name;
  Location location;
  String email;
  Login login;
  Dob dob;
  Registered registered;
  String phone;
  String cell;
  Id id;
  Picture picture;
  String nat;

  Contact(
      {this.gender,
      this.name,
      this.location,
      this.email,
      this.login,
      this.dob,
      this.registered,
      this.phone,
      this.cell,
      this.id,
      this.picture,
      this.nat});

  Contact.fromJson(Map<String, dynamic> json) {
    gender = json['gender'];
    name = json['name'] != null ? new Name.fromJson(json['name']) : null;
    location = json['location'] != null
        ? new Location.fromJson(json['location'])
        : null;
    email = json['email'];
    login = json['login'] != null ? new Login.fromJson(json['login']) : null;
    dob = json['dob'] != null ? new Dob.fromJson(json['dob']) : null;
    registered = json['registered'] != null
        ? new Registered.fromJson(json['registered'])
        : null;
    phone = json['phone'];
    cell = json['cell'];
    id = json['id'] != null ? new Id.fromJson(json['id']) : null;
    picture =
        json['picture'] != null ? new Picture.fromJson(json['picture']) : null;
    nat = json['nat'];
  }
}
// more codes below
NOTE: If you are using generator, be sure to double check variable type in case things gone wrong.

We are only interested in name, picture, and phone number for our main screen. Now we have done model classes, we should create a file to fetch data from randomuser.me API. Inside services folder, create a file called api_fetcher.dart. We will be using http package to request from the link given above and wrap the responses with our model class.

On top of api_fetcher.dart file, import http and dart:convert modules to make use of http modules and parse the JSON file from it. Then, we need to make this class a singleton class since we want to create only one instance of this class in whole session. Dart make it easier to create a singleton class by using factory constructor. Referring to this SO question, we create it like this

// lib/services/api_fetcher.dart

import 'dart:convert';

import 'package:http/http.dart' as http;

import 'package:contacts_sm/models/contact.dart';

class ApiFetcher {
  static final ApiFetcher _singleton = ApiFetcher._internal();
  factory ApiFetcher() => _singleton;
  ApiFetcher._internal();
}

Afterwards, we need to create a fetcher method that call the API using link above and parse the results to JSON, before wrapping all of them into our model class.

// lib/services/api_fetcher.dart

class ApiFetcher {
  static final URL = "https://randomuser.me/api/?results=20";

  static final ApiFetcher _singleton = ApiFetcher._internal();
  factory ApiFetcher() => _singleton;
  ApiFetcher._internal();

  static Future<List<Contact>> fetchContacts() async {
    var response = await http.get(URL);
    List json = jsonDecode(response.body)["results"];
    return json.map((contact)=>Contact.fromJson(contact)).toList();
  }
}

Now here comes scoped model we are going to talk about. We can create our own model and listen to it if there is changes. Create a file called contact_sm.dart in scoped_model folder. Then import scoped_model package as well as our model classes and API fetcher class.

// lib/scoped_model/contact_sm.dart

import 'package:scoped_model/scoped_model.dart';

import 'package:contacts_sm/models/contact.dart';
import 'package:contacts_sm/services/api_fetcher.dart';

Next, we create the class itself named ContactScopedModel which will extend Model abstract class from scoped_model package. We create a private variable that will hold our list of contacts, and its setter and getter. We also will call notifyListeners() once the variable value is changed so any widget that make use of this model can be notified and rebuild themselves with new value.

// lib/scoped_model/contact_sm.dart

import 'package:scoped_model/scoped_model.dart';

import 'package:contacts_sm/models/contact.dart';
import 'package:contacts_sm/services/api_fetcher.dart';

class ContactScopedModel extends Model {
  Future<List<Contact>> _contacts;
  Future<List<Contact>> get contacts => _contacts;
  set contacts(Future<List<Contact>> value) {
    _contacts = value;
    notifyListeners();
  }
}

We also need a method to call the API in this class so that we can use it in our views later. So we will write a method called getContacts which in turn will use fetchContacts method from our API fetcher class.

// lib/scoped_model/contact_sm.dart

import 'package:scoped_model/scoped_model.dart';

import 'package:contacts_sm/models/contact.dart';
import 'package:contacts_sm/services/api_fetcher.dart';

class ContactScopedModel extends Model {
  Future<List<Contact>> _contacts;
  Future<List<Contact>> get contacts => _contacts;
  set contacts(Future<List<Contact>> value) {
    _contacts = value;
    notifyListeners();
  }

  Future<bool> getContacts() async {
    contacts = ApiFetcher.fetchContacts();
    return contacts != null;
  }
}

Now we move to view folder, which will use this scoped model class as a controller. We will make use of ScopedModel widget to pass our model down to other widgets and ScopedModelDescendant widget to find the appropriate ScopedModel in the widget tree. In main.dart inside lib folder, point the MaterialApp home parameter to MainPage screen widget.

// lib/main.dart

import 'package:flutter/material.dart';

import 'screens/main_page.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Contact App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MainPage(),
    );
  }
}

Create a file named main_page.dart inside screens folder then import scoped_model package, as well as our own scoped model and model class.

// lib/screens/main_page.dart

import 'package:flutter/material.dart';

import 'package:scoped_model/scoped_model.dart';

import 'package:contacts_sm/models/contact.dart';
import 'package:contacts_sm/scoped_models/contact_sm.dart';

Create MainPage stateless widget and point the body parameter to ContactList widget.

// lib/screens/main_page.dart

import 'package:flutter/material.dart';

import 'package:scoped_model/scoped_model.dart';

import 'package:contacts_sm/models/contact.dart';
import 'package:contacts_sm/scoped_models/contact_sm.dart';

class MainPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Contacts"),
      ),
      body: ContactList(),
    );
  }
}

Then, create the ContactList stateful widget since we need to make use of its initState to initialize our model and fetch contacts data before anything.

// lib/screens/main_page.dart

class _ContactListState extends State<ContactList> {

  ContactScopedModel contactModel;

  @override
  void initState() {
    contactModel = ContactScopedModel();
    getContacts();
    super.initState();
  }

  getContacts() {
    contactModel.getContacts();
  }

  @override
  Widget build(BuildContext context) { // not yet }
}

In build method above is where we will be using our model to pass the data down to our widgets.

// lib/screens/main_page.dart

@override
Widget build(BuildContext context) {
  return ScopedModel<ContactScopedModel>(
    model: contactModel,
    child: Container(
      child: ScopedModelDescendant<ContactScopedModel>(
        builder: (BuildContext context, Widget child, ContactScopedModel model) {
          return FutureBuilder(
            future: model.contacts,
            builder: (BuildContext context, AsyncSnapshot snapshot) {
              if(!snapshot.hasData) return Center(child: CircularProgressIndicator(),);
              else return ContactListWidget(contacts: snapshot.data);
            }
          );
        },
      ) ,
    ),
  );
}

We wrap ScopedModel widget as parent widget to tell which model to use, and put our contactModel variable as model since we used that earlier to fetch some data. Then we need to use ScopedModelDescendant to find appropriate ScopedModel to be used in child widgets in which, in this case, the ScopedModel we wrapped earlier as parent widget. Since our model has a variable called contacts and it returned a Future, we need to use FutureBuilder widget to get the actual data. Then we pass the data to ContactListWidget, a widget which will list our contacts. We keep this widget separated to avoid too much noise in the codes.

// lib/screens/main_page.dart

class ContactListWidget extends StatelessWidget {

  final List<Contact> contacts;

  ContactListWidget({this.contacts});

  @override
  Widget build(BuildContext context) {

    Contact contact;

    return ListView.builder(
      itemCount: contacts.length,
      itemBuilder: (BuildContext context, int index){

        contact = contacts[index];

        return ListTile(
          leading: CircleAvatar(
            backgroundImage: NetworkImage(contact.picture.thumbnail),
          ),
          title: Text("${contact.name.first} ${contact.name.last}"),
          subtitle: Text(contact.phone),
        );
      },
    );
  }
}

We create a stateless widget ContactListWidget right after it, then we use ListView.builder to build our list, as well as ListTile inside it to create a nice list tile containing image, name, and phone number. The end result should be like the screenshot below.

That’s all for now. We already have a page listing all the contacts we fetched from API and we manage the state using scoped_model. Next, we will add another page containing contact details when we click one of the contact in the list and fine-tune the UI to make it more pleasant. You can find all the codes we have written so far here.

Leave a Reply

Your email address will not be published. Required fields are marked *