Welcome to part 4 of my Flutter App development journal, where I document my process of building a “grocery list” app from scratch.
- Part 1 – Flutter installation and environment setup
- Part 2 – Structuring the app’s main screen UI
- Part 3 – Navigating between app screens with routes
- Part 4 – Refactors and improvements
- Part 5 – Hooking up Firebase to my Flutter project
- Part 6 – Developing the create, read, and update features
- Part 7 – A ton of feature work
- Part 8 – User accounts [someday]
In this post: Advanced interactions, improvements to the mock data, refactoring repeated code into components.
In the previous post, I built and linked up many of the app’s major routes, but there isn’t much to do on them yet. Changes made on one page aren’t really reflected on another, and a lot of code is repeated.
Now that most pages are represented, I wanted to refine the mock data, improve the way it gets passed around, refactor repeated code into components, and add more interactions such as moving items between lists.
This will be a short post, but this work should be done before moving onto the next step, which is hooking the app up to Firebase.
Refactoring the mock data
The mock data was useful for filling in the UI, but now I’d like to refine it.
- Items need to exist independent of any particular list, so that they may appear on multiple lists and be moved between lists with ease
- Lists should be more self-contained so that their data isn’t passed as multiple params; this object should contain the lists’s ID, name, and an array of item IDs
- Default to local storage and pass data around, syncing with server whenever possible, but don’t prevent usage of the app in “offline” mode
The first thing I did was refactor it so the onTap functions all use a callback method passed in from main.dart or existing_shopping_list.dart. This meant I could finally remove all the “if … else” logic that checked what type of list the item belonged to from list_item.dart. (Sorry you had to see that, that was not my finest code, lol)
Here’s kind of a snippet of how that looks, from main.dart:
...
_editList(ShoppingList list) {
Navigator.pushNamed(context, ExistingList.routeName, arguments: ExistingListArguments(list));
}
@override
Widget build(BuildContext context) {
const headerShoppingLists = "Shopping Lists";
const headerStores = "Stores";
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints viewportConstraints) {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
ItemListHeader(text: headerShoppingLists),
ItemList(list: shoppingLists, listType: 'shopping list', onItemTap: _goToList, onInfoTap: _editList),
And then, over here in item_list.dart, we continue passing the onItemTap and onInfoTap method handles down:
import 'package:flutter/material.dart';
import 'add_new.dart';
import 'delete_all.dart';
import 'list_item.dart';
class ItemList extends StatelessWidget {
final list;
final listType;
final onItemTap;
final onInfoTap;
ItemList({Key key, @required this.list, @required this.listType, @required this.onItemTap, @required this.onInfoTap});
@override
Widget build(BuildContext context) {
int getCount(item) {
if (listType == 'shopping list') {
return item.itemIDs.length;
} else {
return list.length;
}
}
return ListView.builder(
shrinkWrap: true, // gives it a size
primary: false, // so the shopping and store lists don't scroll independently
itemCount: list.length + 1,
itemBuilder: (BuildContext context, int index) {
if (index == list.length) {
if (listType == 'crossedOff') {
return DeleteAll();
} else { // store, shopping list
return AddNew(listType: listType);
}
} else {
return ListItem(item: list[index], listType: listType, count: getCount(list[index]), onTap: onItemTap, onInfoTap: onInfoTap);
}
}
);
}
}
And then in list_item.dart, the passed-in methods are invoked on onPressed or on onTap, and this is where I pass back the entire item (we’ll see if I regret that decision later, for now passing the whole item makes it easy to pick it apart on the next page for its ID, name, and contents).
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(buildTitleString(), style: (listType == 'crossedOff' ? TextStyle(decoration: TextDecoration.lineThrough) : TextStyle(decoration: TextDecoration.none))),
subtitle: Text(buildSubtitleString()),
leading: FlutterLogo(),
trailing: IconButton(
icon: Icon(Icons.info),
onPressed: () => onInfoTap(item),
),
onTap: () => onTap(item), //handleTap(context),
);
}
There are some other refactors in this pull request, too:
- I added IDs to the mock data sets and restructured some of them
- I renamed ome of the default Flutter boilerplate (“MyApp” is now “GroceryGoApp”, etc.)
- I moved some existing code into new components, such as delete_all.dart and add_new.dart.
You can view all of these changes here.
On to database stuff
At this point, I can either continue building with mock data, passing it around and modifying it locally but really, it’s time to start putting it into/taking it out of a database. On to part 5!