developers

Build a Flutter Wishlist App, Part 1: Introducing Flutter and Building a Basic Wishlist App

Learn the basics of the Flutter development kit and the Dart programming language and build an app to manage your shopping wishlist.

Mar 31, 202123 min read

In this series of articles, you'll learn how to create a Flutter mobile application that connects to a secured web API. You'll start by building a basic Flutter app that connects to an open API. Once you've written the basic app, you'll learn how to set up the app to use Auth0 to log in. From there, you'll update the app so that it can connect to a secured API. Along the way, you'll learn how to structure your application using a well-known design pattern.

In this article and the one after it, you'll build the basic Flutter app that lets you read, add, edit, and delete items from your wishlist. This basic app won't require the user to log in. You'll add user login and connecting to a secure API in subsequent articles.

Flutter is a UI toolkit from Google that has become a popular choice for building cross-platform applications. As it has its own rendering engine, it enables developers to build applications with a more consistent look and feel on multiple platforms more easily, which is helpful when building applications with branded experiences.

By integrating your apps with Auth0, you can control access to resources and information without having to worry about the details of authentication.

Getting Started

The application featured in this article was written using version 1.22.5 of the Flutter SDK. This was the latest stable version when this tutorial was written. If you need to set up an environment for Flutter development, follow the guide on the official Flutter website.

While the current version of the Flutter SDK at the time of writing is 2.0.3, this article's code is compatible with this SDK.

Applications written under older versions of the Flutter SDK will generally work on newer versions unless breaking changes were introduced. If you run into this situation, consult the release notes; they provide more information on breaking changes and how to migrate your code. Alternatively, you can use an older SDK release, which can be found here.

The application that you'll build will interact with a web API that manages wishlists. In the first part of this article, the app will connect to an API that hasn't been secured. This will make it easier to confirm that the application is able to send and receive information via the API. Later on, the application will connect to a secured API, where you'll use authentication with Auth0.

A template for the API has already been created — all you have to do is generate your own instance. Here's how you do it:

  • Open the Glitch project at https://glitch.com/edit/#!/wishlist-public-api. This is the main version of the API project.
  • Click on the Remix to Edit button in the top-right corner. This will create your own copy of the API project, which you can edit and run. Your project will be assigned a name made of three random words separated by hyphens (e.g., aardvark-calculator-mousse), which will appear in the top left corner of the page.
  • Click on the Share button, which you can find under the project’s name. A pop-up titled Share your project will appear. In the Project links section at the bottom of the pop-up, make a note of the Live site link. This is the root URL of your server, which your application will use to access the API.

The URL of your server will have the form

https://*project-name-here*.glitch.me
, where project-name-here is the name of your Glitch project.

What you'll build

Before we begin, let's take a sneak peek at the Flutter app you'll build:

Here's the screen the user will see when the app launches:

Landing

Here's the screen that displays the wishlist to the user. In this screenshot, it's loading the wishlist items:

Loading Wishlist

And finally, here's the “Add Item” screen, where the user enters the details for an item to be added to the wishlist:

Add Item

Users will sign into the application via an Auth0 login page. Once signed in, they'll be able to manage items within their wishlist.

Flutter apps are written in the Dart programming language. If you have experience with languages like JavaScript, TypeScript, or C#, you'll find that Dart is pretty similar to them.

Create the Project

The first step is to create a new project.

Even though there are IDEs that support Flutter development, we’ll use the command line and the commands provided with the SDK to create a new project. If you followed the official documentation to get your environment up and running, the SDK would have been added to your path. This means that you can create a new project by entering this command:

flutter create --org com.auth0 flutter_wishlist_app

This command creates a new Flutter project named

flutter_wishlist_app
. The
--org
switch specifies that the string “com.auth0” will be used in the application ID for the Android version of the app and as the prefix of the bundle identifier for the iOS version.

After the command is done, you'll have a directory called

flutter_wishlist_app
, where the project's files will be located. Navigate to this directory:

cd flutter_wishlist_app

The project directory contains all the necessary files for a simple starter app that you can run right away. Connect a device to your computer or launch an Android emulator or iOS Simulator, then run the application via the CLI with the following command:

flutter run

You should get a basic application that displays a counter that will increase each time you tap on the plus button:

Counter

Note: there's a difference in the name of the repository, and the name of the application as the Flutter/Dart applications cannot have hyphens in the name.

Clean Up the Starter App

Using your preferred IDE or the command line, open the project directory. It should have the following layout:

Project directory structure

Within the

test
directory is a set of tests that apply to the starter application you just ran. Go ahead and remove all of the files within
test
. You won't be writing tests as part of this tutorial, but it is recommended that you do so for production applications.

Install Dependencies

Our wishlist app makes use of a handful of libraries, packages, and plugins that we'll include in the project as dependencies. They are:

  • http: a library for creating network requests
  • provider: a library that can be used for state management
  • build_runner: a package that is commonly used for code generation
  • json_serializable: generates code for dealing with JSON
  • json_annotation: a companion for the json_serializable package that provides annotations to mark classes that would be involved in handling JSON
  • url_launcher: a plugin for opening URLs

You specify the dependencies used by a Flutter project in its pubspec file. Its name is

pubspec.yaml
, and it's located in the project's root directory.

Open

pubspec.yaml
and replace its
dependencies
section with the following:

dependencies:
  flutter:
    sdk: flutter
  http: ^0.12.2
  json_annotation: ^3.1.1
  provider: ^4.3.2+3
  url_launcher: ^5.7.10

The

dependencies
section of
pubspec.yaml
tells Flutter which dependencies to include when building the app. The numbers after the
http
,
json_annotation
,
provider
, and
url_launcher
dependencies specify which versions should be used.

Note that you didn't add any lines for the

build_runner
and
json_serializable
dependencies. This is because they’re not part of the application. Instead, they’re tools to automate the process of building the application. As such, they’re classified as dev dependencies. You specify their use in the
dev_dependencies
section of
pubspec.yaml
.

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.10.7
  json_serializable: ^3.5.1

Save the changes that you made to

pubspec.yaml
.

Now that you've entered the project's dependencies, it's time to retrieve them from Pub.dev, the package repository for Dart and Flutter applications. The way you'll do this depends on the tools you’re using.

If you’re building the app using an IDE or Visual Studio Code, it may detect that you’ve updated the project's dependencies file and provide you with the options to get the dependencies from Pub.dev. When presented with this option, say yes.

Alternatively, if you’re building the app using a code editor and the command line, get the dependencies by running the following command from within the project's root directory:

flutter pub get

You now have a basic Flutter app project, complete with the dependencies required for the final project. The next step is to set up the classes for storing and sending data.

The Data Service and Data Model Classes

Create the Data Transfer Objects

Our wishlist application will make use of the the following endpoints provided by the web API:

  1. A GET endpoint for retrieving the user's wishlist
  2. A POST endpoint for adding an item to the wishlist
  3. A PUT endpoint for editing an item in the wishlist
  4. A DELETE endpoint for deleting an item from the wishlist

The GET endpoint doesn’t need to be provided with any information. The other endpoints, POST, PUT, and DELETE, require the details of the item to be added, edited, or deleted. This requires us to create data model classes to represent wishlist items. We'll put these classes into their own directory,

lib/models/api
.

To create this directory, add a new directory named

models/api
to the project's
lib
directory.

The first class you'll add to the newly-created

lib/models/api
directory will be
AddItemRequestDTO
, which will represent a request to add an item to the wishlist. The
DTO
suffix denotes that its instances are data transfer objects.

Create a file in

lib/models/api
named
add_item_request_dto.dart
and add the following code to it:

import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';
part 'add_item_request_dto.g.dart';
@JsonSerializable()
class AddItemRequestDTO {
  final String name;
  final String description;
  final String url;
  const AddItemRequestDTO({
    @required this.name,
    @required this.description,
    @required this.url,
  });
  factory AddItemRequestDTO.fromJson(Map<String, dynamic> json) =>
      _$AddItemRequestDTOFromJson(json);
  Map<String, dynamic> toJson() => _$AddItemRequestDTOToJson(this);
}

The

AddItemRequestDTO
class has the following:

  • The properties
    name
    ,
    description
    , and
    url
    , which will hold the name, description, and URL of the item to be added to the wishlist
  • A constructor that sets those properties
  • The methods
    fromJson()
    and
    toJson()
    , which convert the class properties to and from JSON

You may have noticed that

fromJson()
references a function named
_$AddItemRequestDTOFromJson()
and
toJson()
references a function named
_$AddItemRequestDTOToJson()
. Neither of these functions exists...yet. The
@JsonSerializable()
annotation, which appears on the line before the start of the class, indicates that the Dart build system should generate those JSON conversion functions and that they should reside in the file specified in the
part
statement:
add_item_request_dto.g.dart
, where the
g
in the filename means "generated".

Since the

_$AddItemRequestDTOFromJson()
and
_$AddItemRequestDTOToJson()
functions and the
add_item_request_dto.g.dart
file haven’t yet been made, your editor or IDE might point out them out as errors. You can ignore them for now.

The next class to add is

EditItemRequestDTO
, which holds the details for a request to edit an item. Create a file called
edit_item_request_dto.dart
in the
lib/models/api
directory with the following code:

import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';
part 'edit_item_request_dto.g.dart';
@JsonSerializable()
class EditItemRequestDTO {
  final String name;
  final String description;
  final String url;
  const EditItemRequestDTO({
    @required this.name,
    @required this.description,
    @required this.url,
  });
  factory EditItemRequestDTO.fromJson(Map<String, dynamic> json) =>
      _$EditItemRequestDTOFromJson(json);
  Map<String, dynamic> toJson() => _$EditItemRequestDTOToJson(this);
}

Notice that

EditItemRequestDTO
has the same properties and methods as
AddItemRequestDTO
. This is to be expected, as they both deal with wishlist items. Even though they have the same data structure, it's best to keep separate classes for “add” and “edit” requests as they are used to reach different endpoints. This reduces the impact on the code and testing in case one endpoint changes, but the other doesn't.

Just like

AddItemRequestDTO
,
EditItemRequestDTO
uses the
@JsonSerializable()
annotation to specify that the functions to convert its fields to and from JSON should be auto-generated and stored in a file named
edit_item_request_dto.g.dart
.

The last class to add,

ItemDTO
, represents an item in a user's wishlist. Create a file named
item_dto.dart
in the
lib/models/api
directory with the following code:

import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';
part 'item_dto.g.dart';
@JsonSerializable()
class ItemDTO {
  final String id;
  final String name;
  final String description;
  final String url;
  const ItemDTO({
    @required this.id,
    @required this.name,
    @required this.description,
    @required this.url,
  });
  factory ItemDTO.fromJson(Map<String, dynamic> json) =>
      _$ItemDTOFromJson(json);
  Map<String, dynamic> toJson() => _$ItemDTOToJson(this);
}

ItemDTO
is similar to
AddItemRequestDTO
and
EditItemRequestDTO
. The only notable difference is that
ItemDTO
contains an additional property:
id
, which holds the ID of the wishlist item.

With that done, you have finished modelling all of the data that will be sent to and from the APIs. Enter the following on the command line to generate the code that allows the

AddItemRequestDTO
,
EditItemRequestDTO
, and
ItemDTO
classes to convert their data to and from JSON:

flutter pub run build_runner build

Once the command has finished running, the build system will have created all the files with the

g.dart
extension that were referenced in our code, along with the functions they contain. This should resolve the errors that your editor or IDE may have highlighted in
AddItemRequestDTO
,
EditItemRequestDTO
, and
ItemDTO
.

If you are interested in reading more on JSON serialization for Flutter applications, a good place to start is the JSON and serialization page on the Flutter site.

Create the Domain Models

Although you have created models for the data that is sent to and from the APIs, they may contain more information than needed. This could be solved through the use of domain models that better represent and limit the data that the application actually deals with. Doing so also provides an anti-corruption layer, as referred to in domain-driven design. This helps isolate changes that could occur if the schema of the request or response body of an API changes.

Conceptually, users are only concerned with two things:

  1. The wishlist
  2. Items within the wishlist

You’ll need to create classes for both of these.

Items within the wishlist will be represented by instances of the

Item
class. Create a file named
item.dart
in the
lib/models
directory that contains the following code:

import 'package:flutter/foundation.dart';
class Item {
  final String name;
  final String description;
  final String url;
  final String id;
  const Item({
    @required this.name,
    @required this.description,
    @required this.url,
    this.id,
  });
}

The properties should be self-explanatory. Note that

id
is optional; that's because its value will be set only for existing items. When adding new items, the value for
id
will not be specified.

The wishlist, which will contain a collection of items, will be represented by

Wishlist
class. Create a file named
wishlist.dart
in the
lib/models
directory, with the code shown below:

import 'item.dart';
class Wishlist {
  final List<Item> items;
  Wishlist(this.items);
}

Create a Service to Manage the Wishlist

With the DTOs and domain models in place, you are now ready to create the code that will communicate with the APIs. This will be implemented in the

WishlistService
class, which is responsible for managing the user's wishlist. This class puts the code for making the web requests in a single, centralized place, which encourages reuse and facilitates the writing of tests as well.

Create a subdirectory of

lib
named
services
. The project will now have a
lib/services
directory. Create a file named
wishlist_service.dart
in
lib/services
and put the following code inside it:

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/api/add_item_request_dto.dart';
import '../models/api/edit_item_request_dto.dart';
import '../models/api/item_dto.dart';
import '../models/item.dart';
import '../models/wishlist.dart';
class WishlistService {
  static const String itemsApiUrl = 'YOUR_ITEMS_API_URL';
  WishlistService();
  Future<Wishlist> getWishList() async {
    // TODO: send additional info to be able to access protected endpoint
    final http.Response response = await http.get(itemsApiUrl);
    if (response.statusCode == 200) {
      final List<Object> decodedJsonList = jsonDecode(response.body);
      final List<ItemDTO> items = List<ItemDTO>.from(
          decodedJsonList.map((json) => ItemDTO.fromJson(json)));
      return Wishlist(items
          ?.map((ItemDTO itemDTO) => Item(
              id: itemDTO.id,
              name: itemDTO.name,
              description: itemDTO.description,
              url: itemDTO.url))
          ?.toList());
    }
    throw Exception('Could not get the wishlist');
  }
  Future<String> addItem(Item item) async {
    final AddItemRequestDTO addItemRequest = AddItemRequestDTO(
        name: item.name, description: item.description, url: item.url);
    // TODO: send additional info to be able to access protected endpoint
    final http.Response response = await http.post(itemsApiUrl,
        headers: <String, String>{
          'Content-Type': 'application/json',
        },
        body: jsonEncode(addItemRequest.toJson()));
    if (response.statusCode == 201) {
      return response.body;
    }
    throw Exception('Could not add item');
  }
  Future<String> editItem(Item item) async {
    final EditItemRequestDTO editItemRequest = EditItemRequestDTO(
        name: item.name, description: item.description, url: item.url);
    // TODO: send additional info to be able to access protected endpoint
    final http.Response response = await http.put('$itemsApiUrl/${item.id}',
        headers: <String, String>{
          'Content-Type': 'application/json',
        },
        body: jsonEncode(editItemRequest.toJson()));
    if (response.statusCode == 200) {
      return response.body;
    }
    throw Exception('Could not add item');
  }
  Future<void> deleteItem(Item item) async {
    // TODO: send additional info to be able to access protected endpoint
    final http.Response response = await http.delete(
      '$itemsApiUrl/${item.id}',
      headers: <String, String>{
        'Content-Type': 'application/json',
      },
    );
    if (response.statusCode != 204) {
      throw Exception('Could not delete item');
    }
  }
}

The

WishlistService
class has the following methods:

  • getWishlist()
    : Returns the wishlist. A successful request is indicated by the 200 status code contained within the response, after which the JSON response is deserialized to a list of
    ItemDTO
    objects. Each
    ItemDTO
    object is then “mapped” to an instance of the
    Item
    class. The
    Wishlist
    would then hold all of the
    Item
    s in the user's wishlist returned by the method. The method is asynchronous, so its return type is
    Future<Wishlist>
    .
  • addItem()
    : Adds the item specified by the
    item
    parameter to the user’s wishlist. The
    item
    is mapped to an instance of the
    AddItemRequestDTO
    class. The
    AddItemRequestDTO
    object needs to be serialized so that it’s in JSON format; this is done by the
    jsonEncode(addItemRequest.toJson()))
    portion of the code.
  • editItem()
    : Edits the details of the item with the specified ID. The process is similar to the one in
    addItem()
    , except the
    item
    is mapped to an instance of the
    EditItemRequestDTO
    class.
  • deleteItem()
    : Deletes the specified item from the user’s wishlist.

In the code above, the value of the constant

itemApiUrl
field is set to the value
'YOUR_ITEMS_API_URL'
. Replace this with the URL to the
items
endpoint for your Glitch Wishlist API project. Initially, this URL will be of the form
https://*project-name*.glitch.me/api/wishlist/items
where project-name is the name of your copy of the project. For example, if your project's name is aardvark-calculator-mousse, you should change the value of
itemApiUrl
to
https://aardvark-calculator-mousse.glitch.me/api/wishlist/items
.

Later on, you'll be connecting with a version of the API where all the endpoints are protected. This will need additional code, and

TODO
comments have been left in the code for this purpose.

Note that all the methods in

WishlistService
contain some exception handling code. It's been included for the later version of this project when the mobile app communicates with a secured Wishlist API.

With the data service and data model classes set up, it's time to work on the user interface.

Setting Up the User Interface

The application you'll be building will consist of three pages:

  1. A landing page that is shown when the user needs to log in
  2. A page that displays the items in the user’s wishlist
  3. A page where the user can enter the name, description, and URL of an item to be added to the wishlist or edit the name, description, or URL of an existing wishlist item

Create a

pages
directory as a subdirectory of the
lib
directory of the project. This will result in a
lib/pages
directory, where the code for each page will reside.

To allow for the business logic to be decoupled from each page, the application will be built following the Model-View-ViewModel (MVVM) pattern. Following this pattern enables business logic to be tested in isolation and makes it easier to create reusable widgets, which is what UI components are called in Flutter.

With this application, the views are the pages, and the view models represent abstractions of these pages. There will be a corresponding method in the view model for every command that the user can trigger via the user interface (e.g., pressing a button).

The view model will also contain the data that needs to be presented to the user. For this reason, it’s important for there to be a mechanism for views to subscribe to updates in their corresponding view models. This is where the

provider
package will come in, as you'll see later.

The Landing Page

The landing page is where users will log into the application. While the user is in the midst of signing in, it would also be useful to indicate via the user interface that is busy doing so.

We’ll need a place to store the files for the landing page view and view model. With this in mind, create a

landing
directory under
lib/pages
.

The Landing Page View Model

Create a file for the landing page view model named

landing_view_model.dart
in the newly-created
lib/pages/landing
directory. Add the following code to the file:

import 'package:flutter/foundation.dart';
class LandingViewModel extends ChangeNotifier {
  bool _signingIn = false;
  bool get signingIn => _signingIn;
  Future<void> signIn() async {
    try {
      _signingIn = true;
      notifyListeners();
      await Future.delayed(Duration(seconds: 3), () {});
      // TODO: handle signing in
    } finally {
      _signingIn = false;
      notifyListeners();
    }
  }
}

Classes that extend the Flutter SDK class

ChangeNotifier
can notify objects that register as listeners of any changes. By having
LandingViewModel
extend
ChangeNotifier
, the landing page object can "know" if the user is or is not in the process of signing in, which is indicated by the private instance variable named
_signingIn
. This variable is private in order to prevent external code from being able to change its value directly. Instead, only the
signingIn
getter is publicly visible, making it a read-only property to outside objects.

The

signIn()
method is invoked when the user makes an attempt to sign in. When that happens, we set the
_signingIn
instance variable to
true
and call
ChangeNotifier
's
notifyListeners()
method to let the application know that there’s been a change that should be reflected in the user interface.

For now, you'll build a basic version of the mobile app that simulates the process of signing in with an artificial delay created by this line of code:

await Future.delayed(Duration(seconds: 3), () {});

This delay allows you to see how the landing page will look before you implement sign-in functionality.

The Landing Page View

With the landing page view model complete, it's time to work on the corresponding view. Create a file named

landing_page.dart
in the
lib/pages/landing
directory and enter the following code into it:

import 'package:flutter/material.dart';
import 'landing_view_model.dart';
class LandingPage extends StatelessWidget {
  static const String route = '/';
  final LandingViewModel viewModel;
  const LandingPage(
    this.viewModel, {
    Key key,
  }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Wishlist'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            const Text(
              'Welcome to your wishlist.',
              textAlign: TextAlign.center,
            ),
            const Text(
              'Sign in to get started.',
              textAlign: TextAlign.center,
            ),
            if (viewModel.signingIn) ...const <Widget>[
              SizedBox(
                height: 32,
              ),
              Center(child: CircularProgressIndicator()),
            ],
            const Expanded(
              child: SizedBox(
                height: 32,
              ),
            ),
            RaisedButton(
              onPressed: viewModel.signingIn
                  ? null
                  : () async {
                      await signIn(context);
                    },
              child: const Text('Sign in with Auth0'),
            ),
          ],
        ),
      ),
    );
  }
  Future<void> signIn(BuildContext context) async {
    await viewModel.signIn();
    // TODO: navigate to wishlist page
  }
}

With its view and view model defined, the landing page can now be displayed. In order to do this, you'll need to modify the app's entry point,

main.dart
. It's located in the
lib
directory.

Replace the code inside

main.dart
with the following:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.dart';
import 'pages/landing/landing_page.dart';
import 'pages/landing/landing_view_model.dart';
Future<void> main() async {
  // TODO: change initial route based on if user signed in before
  final String initialRoute = LandingPage.route;
  runApp(
    MultiProvider(
      providers: <SingleChildWidget>[
        // TODO: register other dependencies
        ChangeNotifierProvider<LandingViewModel>(
          create: (BuildContext context) => LandingViewModel(),
        ),
      ],
      child: MyApp(initialRoute),
    ),
  );
}
class MyApp extends StatelessWidget {
  final String initialRoute;
  const MyApp(
    this.initialRoute, {
    Key key,
  }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Wishlist',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      initialRoute: initialRoute,
      onGenerateRoute: (RouteSettings settings) {
        switch (settings.name) {
          case LandingPage.route:
            return MaterialPageRoute(
              builder: (_) => Consumer<LandingViewModel>(
                builder: (_, LandingViewModel viewModel, __) =>
                    LandingPage(viewModel),
              ),
            );
        }
        return null;
      },
    );
  }
}

Let's take a look at the code you've just added.

Within the

main()
function is an
initialRoute
variable that represents the route to present to the user when they first run the application. A route can be thought of as a destination that presents a widget (once again, widgets are UI components).

To register the dependencies used by the code, this is done by using the

provider
package. Applications will typically have multiple dependencies that need to be registered. This is represented here via the use of the
MultiProvider
widget that wraps around the
MyApp
class that represents the application itself. In other words,
MyApp
is the child of the
MultiProvider
widget. For now, you're registering one dependency, and that is the view model for the landing page. By using the
ChangeNotifierProvider
from the
provider
package, allows a
ChangeNotifier
instance (the
LandingViewModel
) to be provided to descendent widgets.

The

MyApp
class extends the
StatelessWidget
class, which defines a user interface by building a collection of other widgets that provide a more detailed description of the user interface. In this case,
MyApp
uses
StatelessWidget
's
build()
method to return a
MaterialApp
widget, which specifies that the app should be built using Google's Material Design.

In instantiating the

MaterialApp
widget, the
initialRoute
parameter specifies which widget should be shown when the app launches. This will be either the landing page or the wishlist page. The
routes
parameter is a
Map
that essentially is a lookup table that connects named routes to widgets, each of which represents a page.

When the app starts up, the value passed in the

MaterialApp
constructor’s
initialRoute
parameter is set to
LandingPage.route
. The app then looks up this route in the table provided in the
MaterialApp
constructor’s
routes
parameter to find the corresponding value, which is a function that builds the landing page and displays it via an animated transition.

You may have noticed that in the

routes
table, the page corresponding to
LandingPage.route
is wrapped by a
Consumer
class. This is a widget from the
provider
package, and using it ensures that the landing page has access to its associated view model,
LandingViewModel
. The
Consumer
widget also helps to re-render the landing page via the
builder
property in response to change notifications being sent by its view model.

Structuring the code to manage the dependencies this way provides another benefit. Should a page need to be presented to a user again, a new instance of the associated view model is created. This prevents the application from showing stale data from the last time that page was shown.

You should now be able to run your application. The landing page should look like this:

Landing

You can see that there’s an application bar with Wishlist as the title. A welcome message and button docked to the bottom of the screen can be used to sign in. Pressing the button calls the method in the view model that displays a circular progress indicator for 3 seconds.

Right now, you have an app that displays a single page and has the necessary underlying models to manage a wishlist. In the next article, you’ll put those models to work and give the user the ability to read their wishlist, as well as add, edit, and delete wishlist items.