CHAPTER 4
We’re just one step away from finishing our Flutter application, which I’m excited about and looking forward to accomplishing. In this final step, we need to create the main screen of our application. This screen will display the list of documents and allow the user to add new ones. This is how the finished screen will look.

Figure 4-a: The main application screen
The code for the app’s main screen will reside within the doclist.dart file, which you can create under the lib\ui subfolder of your app’s main project folder.
Just like we have a menu option on the Document Details screen—which allows us to delete an existing document—we need to have the option to remove all documents from the list and delete them from the database. This is known as Reset Local Data.
To add this option, we need to define a menuOptions array, just like we did with the details screen.
Before doing that, let’s go ahead and reference all the modules and packages we’ll need.
Code Listing 4-a: Creating the main menu—Doclist.dart
import 'dart:async'; import 'package:flutter/material.dart'; import '../model/model.dart'; import '../util/dbhelper.dart'; import '../util/utils.dart'; import './docdetail.dart'; // Menu item const menuReset = "Reset Local Data"; List<String> menuOptions = const <String> [ menuReset ]; |
In the first two lines of code, we import the Dart async package and the Material Design package that comes with the Flutter framework, which contains the UI widgets our application will use.
Next, we import the other project modules we created previously—this is because we’ll need to reference various classes declared within those modules.
Finally, we create menuOptions as a generic List collection (created from an array), which includes only the menuReset option.
Just like we did within Docdetail.dart, we are ready to create the base widget that will be used within the app’s main screen. This is going to be a stateful widget, which will describe the main screen’s user interface.
As mentioned previously, a stateful widget has a state that can change, which is useful when parts of the user interface that are being rendered can change dynamically, given that the items on the list within the main screen can vary.
Let’s define the stateful widget that we will use for rendering the UI of the main screen.
Code Listing 4-b: Stateful widget—main screen
// Previous code… class DocList extends StatefulWidget { @override State<StatefulWidget> createState() => DocListState(); } |
As we can see, it’s very simple—all we do is override the createState method, which will create an instance of the DocListState class.
The DocListState class is where we are going to have the logic that creates and manipulates the main screen’s UI. Let’s explore that now.
Code Listing 4-c: Stateful widget—main screen
// Previous code… class DocListState extends State<DocList> { DbHelper dbh = DbHelper(); List<Doc> docs; int count = 0; DateTime cDate; // More code to follow... } |
The DocListState class will not only be responsible for creating the main screen’s UI, but also for handling its state.
To be able to do that, it’s important to keep track of a few things, such as the list of documents, represented by docs; the number of documents, represented by count; a reference to the database, represented by dbh; and the current datetime, represented by cDate.
Now that we know which variables are going to keep track of the main screen’s state, let’s now initialize it. We can do this as follows.
Code Listing 4-d: State initialization—main screen
// Previous code… class DocListState extends State<DocList> { // Previous code @override void initState() { super.initState(); } } |
All we are doing is overriding the implementation of the initState method, and within it, invoking the initState method from the inherited State<DocList> class.
Awesome—we now have the foundation of our DocListState class laid out. Let’s move on to a more interesting aspect, which is retrieving the data needed to populate the list of documents.
Possibly the most important aspect of the application—at least from a usage point of view—is retrieving the data needed to display the list of documents to expire or that have expired.
Let’s explore the full code of the method that makes this happen.
Code Listing 4-e: Retrieving the data—main screen
// Previous code… class DocListState extends State<DocList> { // Previous code Future getData() async { final dbFuture = dbh.initializeDb(); dbFuture.then( // result here is the actual reference to the database object. (result) { final docsFuture = dbh.getDocs(); docsFuture.then( // result here is the list of docs in the database. (result) { if (result.length >= 0) { List<Doc> docList = List<Doc>(); var count = result.length; for (int i = 0; i <= count - 1; i++) { docList.add(Doc.fromOject(result[i])); } setState(() { if (this.docs.length > 0) { this.docs.clear(); } this.docs = docList; this.count = count; }); } }); }); } } |
What is going on here? The first important aspect to consider is that the getData method is async (runs asynchronously), and it returns a Future object, which is the preferred way of handling data requests. This is done to avoid blocking the application when performing a computation that might be delayed.
For those familiar with JavaScript web development, think of a Future as a construct very similar to a Promise.
The first instruction within getData is to call the initializeDb method from the database helper class. This ensures that the connection with the database is established.
final dbFuture = dbh.initializeDb();
When the connection to the database is established, the then method of the Future object returned is executed.
dbFuture.then(…)
The result of that database connection is passed as a parameter—(result)— to an anonymous function that is executed within the scope of the then method of the Future object returned—dbFuture.
(result) {…}
Within this anonymous method scope, the next thing we do is retrieve the list of documents from the database—this is done by invoking the getDocs method, which also returns a Future object.
final docsFuture = dbh.getDocs();
When the retrieval of the list of documents has been finalized, the then method of the Future object returned is executed.
docsFuture.then(…)
The result of that action—(result)—is the list of documents, which is passed as a parameter to an anonymous function that is executed within the scope of the then method of the Future object returned—docsFuture.
(result) {…}
Within that anonymous function scope, we first check the length of the result returned, which indicates the number of documents in the database.
if (result.length >= 0) {…}
We then create a list of documents using the Doc class, which we will use to populate with the document data retrieved from the database.
List<Doc> docList = List<Doc>();
We also initialize the count variable to the value of result.length, which indicates how many documents were retrieved from the database.
var count = result.length;
Then, we loop through the result object obtained from the database—where each iteration represents a database row—and then convert each row into a Doc object, which we add to docList.
for (int i = 0; i <= count - 1; i++) {
docList.add(Doc.fromOject(result[i]));
}
With the database rows converted to a List<Doc> object, we need to change the state of our DocListState class so that the UI can be rendered. We do this by calling the setState method.
setState(() {…} )
Within setState, the first thing we do is empty the contents of this.docs, which is one of the variables we declared at the beginning of the DocListState class.
if (this.docs.length > 0) {
this.docs.clear();
}
Then, to this.count—which was also declared at the beginning of the DocListState class—we assign the value of count, obtained from result.length.
this.docs = docList;
this.count = count;
As you can see, the logic behind getData is quite simple and easy to follow once you understand the concept of Future objects in Dart, and how results are returned.
Another intrinsic aspect of our application is checking dates, which is important because the goal of the app is to help us keep track of important documents before they expire.
Let’s have a look at the following function, which periodically checks for date and time discrepancies, and if there are any (for example, the phone’s date-time is different the current date-time), executes the getDate method.
This way, we can have the latest document data and expiration dates. Here is the function’s code.
Code Listing 4-f: Check date—main screen
// Previous code… class DocListState extends State<DocList> { // Previous code void _checkDate() { const secs = const Duration(seconds:10); new Timer.periodic(secs, (Timer t) { DateTime nw = DateTime.now(); if (cDate.day != nw.day || cDate.month != nw.month || cDate.year != nw.year) { getData(); cDate = DateTime.now(); } }); }
// More code will follow… } |
As we can see, the code is quite straightforward. It creates a Timer object that executes every 10 seconds. The timer’s execution is performed by invoking the periodic method. Within this method, the cDate object represents the date-time from the moment the main screen widget was created (first rendered), and the current date-time is represented by the nw object.
If there’s a difference between the day, month, or year of both DateTime objects, then the getData method is invoked.
Normally there shouldn’t be a difference between the day, month, or year of when the main screen widget was rendered (cDate) and the date-time of the nw object. This is because shortly after the screen is rendered, the current date-time should be calculated, and its value assigned to nw.
However, there might be cases where the app runs for the first time on a phone that has the wrong date-time settings, and this would result in a wrong computation of the expiry dates for each document. So, the execution of the _checkDate function—after the phone date-time settings have been adjusted—would force a correct computation of the expiry date for each document.
Given that the app’s main screen contains a list of the documents that are in the database, if we would like to edit or delete a specific document, we would need to navigate to it. To do this, we need to define a navigateToDetail method. Let’s see how it looks.
Code Listing 4-g: Navigating to a document—main screen
// Previous code… class DocListState extends State<DocList> { // Previous code void navigateToDetail(Doc doc) async { bool r = await Navigator.push(context, MaterialPageRoute(builder: (context) => DocDetail(doc)) ); if (r == true) { getData(); } } // More code will follow… } |
The navigation works by displaying the details of the document tapped from the document list. This is achieved by using the Navigator.push method and passing a DocDetail instance of the document selected.
If the result (r) of the push method is true—which means that the document has been modified—then the getData method is called to retrieve the latest document information from the database.
Given that we want to have the option to delete all the data stored by the app, we need to add some code that will remove any data stored locally.
Before any removal operations take place, we need to be able ask the user for confirmation, and if it is confirmed, then we can remove the data stored in the database. Let’s have a look at the code.
Code Listing 4-h: Resetting the local data—main screen
class DocListState extends State<DocList> { // Previous code void _showResetDialog() { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: new Text("Reset"), content: new Text("Do you want to delete all local data?"), actions: <Widget>[ FlatButton( child: new Text("Cancel"), onPressed: () { Navigator.of(context).pop(); }, ), FlatButton( child: new Text("OK"), onPressed: () { Future f = _resetLocalData(); f.then( (result) { Navigator.of(context).pop(); } ); }, ), ], ); }, ); } // More code will follow… } |
Let’s have a look at what we are doing here. The _showResetDialog simply invokes the Flutter showDialog method.
The builder property of the showDialog method is assigned to an anonymous method, which receives a BuildContext parameter and returns an AlertDialog instance.
The AlertDialog object contains title, content, and actions properties. The title and content properties are self-explanatory, but the really interesting part is what happens within the actions property.
The actions property is assigned to an array of type Widget, which contains two FlatButton objects. The first button represents the Cancel option, and the second one (OK) represents the reset data option.
Both buttons have a child property that contains the Text object displayed on each. They also contain an onPressed event that triggers specific functionality.
In the case of the Cancel button, the onPressed event executes code that returns the focus to the app’s main screen. This is done by invoking the pop method.
As for the OK button, the onPressed event makes a call to the _resetLocalData method—which is responsible for removing the data from the database—and then returns the control to the app’s main screen, which is also done by invoking the pop method.
Let’s have a look at the logic behind the _resetLocalData method.
Code Listing 4-i: Resetting the local data (2)—main screen
// Previous code… class DocListState extends State<DocList> { // Previous code Future _resetLocalData() async { final dbFuture = dbh.initializeDb(); dbFuture.then( (result) { final dDocs = dbh.deleteRows(DbHelper.tblDocs); dDocs.then( (result) { setState(() { this.docs.clear(); this.count = 0; }); } ); } ); } // More code will follow… } |
A call is made to the initializeDb method, which establishes the connection to the database.
When the connection is established, and within the anonymous function invoked within the then method of dbFuture, the deleteRows method is called, which is responsible for deleting the rows on the database.
The setState function is called to reset the list of documents—this.docs—and set the number of documents to zero—this.count.
Now that we’ve seen how we can reset the information stored in the database, let’s check how we can trigger this functionality manually by selecting the menu option associated with it. Here’s the code.
Code Listing 4-j: Selecting the menu option—main screen
// Previous code… class DocListState extends State<DocList> { // Previous code void _selectMenu(String value) async { switch (value) { case menuReset: _showResetDialog(); } }
// More code will follow… } |
We have a _selectMenu method that includes a switch statement with a condition when the menuReset option has been selected. When that happens, the _showResetDialog method is invoked—super simple.
I decided to use a switch statement because if later there’s a need to add additional menu options, all that would be required would be to add additional case expressions to it.
We are now reaching one of the most interesting, useful, and important parts of our application: building the list of documents that are presented on the app’s main screen. Let’s look at the complete code for this and dissect it, piece by piece.
Code Listing 4-k: Creating the list of documents—main screen
// Previous code… class DocListState extends State<DocList> { // Previous code ListView docListItems() { return ListView.builder( itemCount: count, itemBuilder: (BuildContext context, int position) { String dd = Val.GetExpiryStr(this.docs[position].expiration); String dl = (dd != "1") ? " days left" : " day left"; return Card( color: Colors.white, elevation: 1.0, child: ListTile( leading: CircleAvatar( backgroundColor: (Val.GetExpiryStr(this.docs[position].expiration) != "0") ? Colors.blue : Colors.red, child: Text( this.docs[position].id.toString(), ), ), title: Text(this.docs[position].title), subtitle: Text( Val.GetExpiryStr(this.docs[position].expiration) + dl + "\nExp: " + DateUtils.convertToDateFull( this.docs[position].expiration)), onTap: () { navigateToDetail(this.docs[position]); }, ), ); }, ); }
// More code will follow… } |
To understand this better, let’s go over each part of the code. The first thing that is being done within the docListItems method is a returning of a ListView object that will contain the list of documents that the app is going to display.
To create that list, we need to call the ListView.builder method, which has two important properties: itemCount and itemBuilder.
The itemCount property indicates how many documents are going to be added to the list—notice how we are assigning the value of the count variable that was retrieved from the database.
The itemBuilder property is the one used to build the document list; this is achieved with an anonymous function that receives a BuildContext object as one of its parameters.
Let’s explore the content of the anonymous function assigned to the itemBuilder property—this is where things get interesting.
On the first two lines of the anonymous function, all we are doing is getting the expiry date of each document as a String—this is done by invoking the GetExpiryStr function—and then determining the remaining days left (that will be displayed on the screen).
String dd = Val.GetExpiryStr(this.docs[position].expiration);
String dl = (dd != "1") ? " days left" : " day left";
Notice that the parameter position indicates the current document being added to the list.
With the remaining days determined, the next thing we do is return a Card object, which will contain the document details. Each document is displayed within its own Card object.
To understand better the composition of the Card object, let’s look at the following diagram.

Figure 4-b: Card objects within the document list
The first two properties of the Card object are self-descriptive. The first indicates the color used for the background of the Card, in this case white.
The second indicates whether the Card object has a slight visual elevation with respect to the document list, which is seen as a thin, gray line below each Card object.
The third property of the Card object is its child property, to which we assign a ListTile object. So basically, the actual content of the Card object is determined by a ListTile object.
The ListTile object contains a CircleAvatar object that is red when the document has expired, and blue when the document has not expired. The backgroundColor of the CircleAvatar object is determined by the following ternary conditional expression.
(Val.GetExpiryStr(this.docs[position].expiration) != "0") ?
Colors.blue : Colors.red
The Text (number) contained within the CircleAvatar object indicates the position (order) of the document within the database.
Next, within the ListTile property we have the title property, which indicates the name of the document.
Following that, there’s the subtitle property, which displays the remaining days a document before it expires.
Text(Val.GetExpiryStr(this.docs[position].expiration) + dl +
"\nExp: " + DateUtils.convertToDateFull(
this.docs[position].expiration))
Finally, the ListTile object has an event that gets triggered when a user taps on the document, the onTap event, which basically invokes the navigateToDetail method. This method opens the details of the document selected, which can then be edited.
onTap: () {
navigateToDetail(this.docs[position]);
}
The biggest chunk of our main screen, the document list, is finished. However, we still need to wrap that document list around the main Scaffold object that will hold it, and then Stack it properly so that it displays correctly during runtime (both in portrait and landscape modes).
We’ll also need to add an AppBar object and link our menu option to it. Let’s go ahead and do all this.
Code Listing 4-l: Finishing the main screen
// Previous code… class DocListState extends State<DocList> { // Previous code @override Widget build(BuildContext context) { this.cDate = DateTime.now(); this.docs = List<Doc>(); getData(); } _checkDate(); return Scaffold( resizeToAvoidBottomPadding: false, appBar: AppBar( title: Text("DocExpire"), actions: <Widget>[ PopupMenuButton( onSelected: _selectMenu, itemBuilder: (BuildContext context) { return menuOptions.map((String choice) { return PopupMenuItem<String>( value: choice, child: Text(choice), ); }).toList(); }, ), ] ), body: Center( child: Scaffold( body: Stack( children: <Widget>[ docListItems(), ]), floatingActionButton: FloatingActionButton( onPressed: () { navigateToDetail(Doc.withId(-1, "", "", 1, 1, 1, 1)); }, tooltip: "Add new doc", child: Icon(Icons.add), ), ), ), ); } } |
Just like we did with the Document Details screen, to render its content we need to override the build method inherited from the DocListState class. Let’s explore each part of the build method so we can understand exactly what it does.
The first thing that happens within the build method is that we get the current date-time, which will represent the date-time when the main screen is rendered. This value is stored within the cDate variable we previously explained.
Next, if the document list doesn’t exist yet—which means that no documents have ever been retrieved—then the list of documents is initialized, and a call to the getData method is made to check whether there are any documents within the database.
if (this.docs == null) {
this.docs = List<Doc>();
getData();
}
Following that, we invoke the _checkDate method. This is to check that the current date-time is aligned with the date-time the screen was rendered, checking that the amount of days left for each document is accurate.
Then, the build method returns a Scaffold object, which will render the main screen. The Scaffold object has two main properties that take most of the logic of the build method code: the appBar and the body.
The appBar property is assigned to an AppBar object, which basically constitutes the application’s top navigation bar, including the app’s name and the menu option. To understand it better, let’s look at the following diagram.

Figure 4-c: The main screen’s AppBar
As we can see, the actions property of the AppBar object is nothing more than an array of type Widget, which includes a PopupMenuButton object.
The PopupMenuButton object contains an onSelected property that is assigned to the _selectMenu method, which opens the Reset Local Data option.
The menu itself is built by an anonymous function that is assigned to the itemBuilder property and returns a menuOptions array object as a list, with each menu item being a PopMenuItem object (in our case, there’s only one menu option).
By doing it this way, we could expand the application later and add extra menu options to the menuOptions array without having to modify any of the rendering functionality.
To better understand its composition of the body property, let’s have a look at the following diagram.

Figure 4-d: The main screen’s body
We can see that the Scaffold object contains the document list and a floating button. The Scaffold object is wrapped within a Center object, so that all the content is properly centered in any rotation that the device is used.
The document list generated by the docListItems method is wrapped around a Stack widget, which is done to ensure that the list of documents is positioned correctly relative to the edges of its surrounding box.
The floating button used to create a new document is created by the FloatingActionButton object—assigned to the floatingActionButton property—and it contains an Icon object, a tooltip property, and an onPressed event. This event calls the navigateToDetail method that displays an empty Document Details screen when creating a new document.
That wraps up the app’s main screen. If you recall, the main screen (represented by the DocList class) is invoked within main.dart.
If you have followed all the steps described, you should now be able to run the application from Android Studio (don’t forget to select an emulator device).
A minute later (sometimes Android Studio takes a minute or so to resolve and update all the required dependencies), you’ll be able to see the app running with no documents in it.

Figure 4-e: The finished app running (clean database)
The first time it runs, the app shows no documents because the database is empty, and just newly created. You should now be able to add your own documents.
I’m going to add a new test document with the following data. Let’s have a look.

Figure 4-f: A new document
If we click Save, the document will appear on the app’s main screen—we can see this in the following figure.

Figure 4-g: The new cocument zdded
Awesome—it’s working as expected. Now, let’s try the Reset Local Data menu option to see if the database is cleared.

Figure 4-h: The reset cata option
If we tap on the Reset Local Data menu option, we should see the following dialog box.

Figure 4-i: Confirming the reset data option
If we now tap OK, the contents of the database should be deleted and the document list should appear empty. Let’s see if that’s the case.

Figure 4-j: An empty document list
Awesome—the Reset Local Data menu option worked, and we have a fully working application.
You can find the full source code and Android Studio project for this application on the Syncfusion GitHub repository that comes along with this book. Alternatively, you may also get the full source code files here.
We’ve now explored the Flutter framework and built this cool application together—and although we’ve reached the end of the book, this is hopefully just the beginning of your journey with Flutter.
Going forward, I encourage you to expand the capabilities of this application and, if I may, I’d like to suggest you add a cool feature to this application that I couldn’t cover in this book. That’s the ability to have a mechanism in place that allows the app to keep multiple document lists, each associated with an email address, which can be synced to the cloud (i.e. using Google’s Cloud Firestore database).
Say, for instance, that I’d like to keep a list of documents for myself, but I’d also like to keep a list of documents for my wife—with documents that are relevant to her.
If the app would give me the option to store two lists, each one associated with a different user (email address), I could keep track of multiple document lists and have them synced to the cloud in case my phone gets lost, damaged, or stolen.
This would also give me the capability of installing the app on a new phone, and being able to retrieve those document lists by using the username (email address) each was saved with—wouldn’t that be cool?
Also, why not add the alerting mechanism for which we built the UI—this would also be something very useful.
So, there you go—a interesting challenge and problem to solve, which can be used to expand the code that has already been written for this application.
Before you go, don’t forget to check the Appendix, where you can find the finished source code of each Dart file of the application.
I’m keen to see what you will build. Until next time, have fun with Flutter. Thanks so much for reading!