Building a Flutter App from Scratch: UI and State Management
Written on
Chapter 1: Introduction to Flutter App Development
This piece is the third installment in a series about constructing a Flutter application from the ground up. Previously, we concentrated on establishing models that meet the app's specifications. We also set up a database to facilitate data reading and writing operations.
In this article, we will connect our basic back-end to the user interface, enhancing the app's functionality to allow user interactions.
Prefer a comprehensive ebook over multiple articles? You can download the ebook for free from my Gumroad store!
Disclaimer
This article outlines just one of many possible approaches to achieve our goals. There are numerous alternatives that may be more suitable or easier to understand. I encourage you to keep an open mind and experiment with different methods to discover what works best for you.
Packages Utilized in This Article
- Riverpod for state management
- Get for dependency injection
Chapter 2: Enhancing the App with Dependency Injection
One of the primary improvements we will implement is Dependency Injection. There are various methods to accomplish this, and the best approach often depends on personal preference. Given the simplicity of our app, our focus is on creating a testable environment. This requires a mechanism to mock dependencies, allowing us to test small code segments without running the entire application.
The most straightforward approach involves accessing all dependencies from a centralized source. The Get package provides this functionality. Below is the setup:
Our application utilizes a NavigationService and a DatabaseService, both of which are frequently accessed. We register these services using the putAsync() method, enabling us to access them with Get.find() whenever necessary. You will encounter this pattern in the following sections. We initialize these dependencies as early as possible, such as within the main() method before launching the app.
During unit tests, we can substitute real classes with mocks using a custom setup method, which is all we require for now.
State Management with Riverpod
If you are unfamiliar with Riverpod or have yet to try it, I recommend checking out the Riverpod documentation. We will leverage it for state management in this application. Riverpod offers various types of providers that allow us to retrieve and modify data. The user interface listens to these providers and automatically updates in response to changes. All providers are immutable and can be globally defined, though it's essential to organize them into separate files as their number increases. This innovative approach can be somewhat challenging to grasp initially.
Fetching Initial Data
We start with a provider that retrieves all data from the database. This provider employs the DatabaseService to fetch all Task objects. Integration into the user interface can be illustrated as follows:
Instead of using a StatelessWidget or StatefulWidget, we will utilize a ConsumerWidget or a ConsumerStatefulWidget. The build method now accepts an additional parameter, WidgetRef, which we can use to access our providers. A FutureProvider presents convenient methods to handle various states, enabling us to display the appropriate widgets to the user. Initially, a CircularProgressIndicator is displayed, followed by a ListView containing a Card widget for each data object, or a simple error message if retrieval fails. Clicking an item directs the user to the details page.
An overview of all available providers in Riverpod can be found here.
Retrieving a Specific Object
To modify existing data, we need to access a single Task item. Using Riverpod and its providers, we can accomplish this:
This provider utilizes the family constructor, allowing us to pass additional arguments—in this case, an identifier for a Task object. We can also employ our other provider (via the ref argument) that returns all Task objects, enabling us to filter for the correct element. Let’s take a look at the view:
Again, observe the handy when() method of the provider for managing various states. I find it quite beneficial! 😊 If our singleTaskProvider were merely an instance of the Provider class, those methods wouldn't be accessible. It needs to be a FutureProvider. The view itself largely consists of text, and clicking on the FloatingActionButton allows navigation to the edit page.
Modifying Data
The edit page comprises basic text field widgets and switches for data manipulation. A button initiates the save operation, structured as follows:
A new Task object is created (it is immutable, thus we cannot edit it) and stored in the database. The method manages either the modification of existing data or the creation of new entries. Upon saving, the app navigates back to the previous page. As demonstrated in the snippets from home_view.dart or task_view.dart, the call ref.refresh(taskListProvider) triggers a refresh of the user interface to reflect the new data.
Conclusion
In this article, we established a connection between our basic back-end and the user interface while enhancing the app to facilitate user actions. In the next installment, we will implement the logic for reminders and integrate them into our application.
The source code is accessible on GitHub, and as the project develops, you may find variations in the repository.
Your next stop is the fourth part of this series. Enjoy! 🎉
Previous articles from the series:
In this video, learn how to create a Flutter app with a REST API, focusing on building a news app's UI from scratch.
This tutorial guides you through creating your first Flutter app with the help of a Codelab, covering essential concepts and hands-on exercises.