This summer I took a break from side projects and gave LeetCode a try. It’s been nearly 3 years since my data structures and algorithms course with OSU, so I wanted to give myself a refresher on solving these kinds of problems.
This particular problem, “Generate Parenthesis“, was challenging for me at first but I revisited it a couple times over the summer and on the most recent try, I developed a solution that I think is fairly simple to understand, so I wanted to share it here. (I admit it’s not the most performant solution, but it passes.)
The premise
Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.
Example 1:
Input: n = 3
Output: ["((()))","(()())","(())()","()(())","()()()"]
Example 2:
Input: n = 1
Output: ["()"]
Constraints:
1 <= n <= 8
My solution
I started this problem by writing out what would be generated when n =1, when n = 2, when n = 3, and when n = 4. This step helped me see what I needed to generate and it gave me an opportunity to spot patterns.
When n = 0 result = [""]
When n = 1 result = ["()"]
When n = 2 result = ["()(), (())"]
When n = 3 result = ["()()()","((()))","()(())","(())()"]
When n = 4 result = ["()()()()","(((())))","(()(()))","((())())","()((()))","((()))()","()()(())","()(())()","(())()()"]
The pattern:
I noticed that every time I generated a new result, I was building on the previous n value’s result array.
Specifically, the steps are:
Take each result in the previous n value’s array and wrap it in parentheses. Each of those “wrapped” results becomes part of the new n value’s results array.
Take each result in the previous n value’s results array and insert a new “()” at each possible location in the string. Each of those “inserted” results becomes part of the new n value’s results array.
You might realize at this point that some duplicate results will be generated during this process. This is why I put the results into a Set (so that duplicates are ignored).
Here is this approach, written in TypeScript:
function generateParenthesis(n: number): string[] {
if (n === 1) { return ["()"]; }
let result = new Set<string>();
let previousCombos = generateParenthesis(n-1);
for (let i in previousCombos) {
let combo = previousCombos[i];
// now step through each one and embed a new paren at each possible position
for (let i = 0; i < combo.length; i++) {
result.add(combo.substring(0,i) + '()' + combo.substring(i));
}
}
return [...result];
};
Honestly, I don’t fully understand the runtime on this one, since it’s recursive and since each subsequent step draws from a growing list of “previous step” solutions. It “feels” exponential, but it’s not quite – or at least, the quantity of results in the sets that I worked out by hand is not increasing exponentially.
Pressed to come up with something, I’d say it’s O(2n) because:
We have to “handle” every 1…n (that’s the first n)
And every time we “handle” an n, we have to produce n * x results, where x is the length of the previous n’s results and that length grows (considerably) with each step.
From this article, “The growth curve of an O(2n) function is exponential – starting off very shallow, then rising meteorically.”
Hey, that sounds like this problem… right?
(If you know how to figure this one out with greater accuracy I’d love to hear it in the comments.)
Performance
[Update] 1 year later: a dynamic programming solution
November 2022 – I showed this blog to a friend and he developed a neat solution that builds a 2D array where each index holds all of the previous step’s calculations. What’s clever about this solution is that it does not generate duplicates the way my Set-based solution above does.
function nextPermutation(previousPermutations: string[][]): string[] {
const nextPermutationsLength = previousPermutations.length;
let nextPermutations = [];
for (let i = 0; i < nextPermutationsLength; i++) {
// each of the previous permutations becomes our "inner" permutations (array)
const innerPermutations = previousPermutations[i];
// calculate how many outer permutations we need based on where we are in the loop
const outerPermutationsLength = nextPermutationsLength - 1 - i;
// grab from that position to get "outer permutations"
const outerPermutations = previousPermutations[outerPermutationsLength];
// now that we have the outer permutations, step through them...
let newPermutations = outerPermutations.flatMap(outer => innerPermutations.map(inner => `(${inner})${outer}`));
newPermutations.forEach((newPerm) => {
nextPermutations.push(newPerm);
})
}
return nextPermutations;
}
function generateParenthesis(n: number): string[] {
let result = [[""]];
for (let i = 1; i < n+1; i++) {
// push the array of new permutations into the result array
result.push(nextPermutation(result));
}
// result[n] now holds the array we need...
return result[n];
};
In a social group, there are n people, with unique integer ids from 0 to n-1.
We have a list of logs, where each logs[i] = [timestamp, id_A, id_B] contains a non-negative integer timestamp, and the ids of two different people.
Each log represents the time in which two different people became friends. Friendship is symmetric: if A is friends with B, then B is friends with A.
Let’s say that person A is acquainted with person B if A is friends with B, or A is a friend of someone acquainted with B.
Return the earliest time for which every person became acquainted with every other person. Return -1 if there is no such earliest time.
Example test case:
Input: logs = [[20190101,0,1],[20190104,3,4],[20190107,2,3],[20190211,1,5],[20190224,2,4],[20190301,0,3],[20190312,1,2],[20190322,4,5]], n = 6
Output: 20190301
My solution, in Typescript:
function earliestAcq(logs: number[][], n: number): number {
// sort the logs by timestamp (earliest first)
logs.sort((a:number[], b:number[]) => {
return (a[0] - b[0]);
});
// this map is going to serve as a container for all the sets
let mapOfSets = new Map();
// put every friend ID into its own individual set
for (let i = 0; i < n; i++) {
let individualSet = new Set();
individualSet.add(i);
mapOfSets.set(i, individualSet);
}
// now every friend is its own set in the map
// console.log(mapOfSets);
// step through the logs and join the sets as matches are encountered
for (let i = 0; i < logs.length; i++) {
let friendA = logs[i][1];
let friendB = logs[i][2];
// friendA and friendB have "met" - get their sets
let setA = mapOfSets.get(friendA);
let setB = mapOfSets.get(friendB);
// and if their sets are not equal...
if (setA != setB) {
// join the two sets by adding all of the friends from setB to setA
for (let friend of setB) {
// add all the friends from setB to setA
setA.add(friend);
// update map of sets such that setA is pointed at by each friend originally from B
mapOfSets.set(friend, setA);
}
if (setA.size === n) {
// every friend is now accounted for in set A
// return the timestamp where this was achieved
return logs[i][0];
}
}
}
return -1;
};
Sort all the inputs by timestamp so the earliest ones are first
Create a new map and fill it with sets representing the currently known friendships. The keys will be the friend “ID numbers”, and the values will be the sets representing who that friend is friends with. In the beginning, a friend is only friends with themselves. mapOfSets = {0:{0}, 1:{1}, 2:{2}, 3:{3}, 4:{4}, 5:{5}}
Now, step through the logs and grab each friend pair (friendA and friendB) from that log.
Get the set associated with friendA, and the set associated with friendB, and see if they’re identical
If the sets are not identical, copy all the friends from setB to setA and for each of those relocated friends, update their own value in the mapOfSets to point at setA
If setA becomes equal in size to n (the number of friends), then this is the moment where “everyone knows everyone else” and we can return the timestamp of the current log
Runtime analysis:
O(n log n) due to the sorting
Performance:
Ehh, I doubt this was the absolute best way to do this – it’s just because so few other Typescript submissions exist.
This post is part of my “Building a Flutter App” dev journal, which begins here.
In this post: I develop a feature that allows a user to manually re-order a shopping list. Taking inspiration from the way Spotify lets users re-order a playlist, Grocery Go’s users should be able to put the items in any order they like.
This gif demonstrates the feature. The user re-orders items in a list and changes which “store” is active to set different item orders for different stores.
Getting set up – where we’re starting from
When I started work on this feature, the user could already create a shopping list and populate it with items, like so:
However, there is no way to sort or re-order those items.
As a user, I think it’d be useful if the items could be re-ordered to match the order I actually pick them up in when I’m at the store.
But which store? I shop at many different stores, and they’re all laid out differently, so in addition to creating a default order I also want to be able to create variations on the default order and “save” those variations to each store that this list applies to.
Planning the work: UI inspiration
For inspiration, I looked at how Spotify’s phone app (or at least iOS app) gives the user the ability to reorder items in a list.
To change the order of items in this playlist, the user taps the “three dots” adjacent to the “down arrow” above the “add songs” button.
A separate screen pops up and offers the option to “Edit” the playlist:
Tapping “Edit” opens up a modal in which the individual songs can be pressed on and dragged up and down in the list.
In this screenshot, I am dragging the song named “Passage” up in the list. It follows my finger as long as I keep pressing the screen.
Feature list
Every shopping list has a “default” order
Every shopping list can be linked with 1 or more stores, and each of those store links has its own item order (initially copied from ‘default’)
The user can re-order items in the “default” list and the store-specific lists
The user can change which store list is being displayed via the shopping list view
Creating a brand new item adds it to the end of the “default” list as well as to the end of all store-specific lists
Modeling the data
The user can store different item sequences for different stores, like so:
Default – items are, by default, shown in the order they were created for this list. The user can re-order this list, though. Newly created items are added to the bottom (end) of this list.
Party size bag of M&Ms
Bread
Bananas
Milk
Eggs
Safeway – the user wants to see the items in this order when they are at Safeway. Newly created items are added at the bottom of this list.
Bananas
Bread
Milk
Eggs
Party size bag of M&Ms
In the Firebase data structure, I imagined each item would have a map of key/value pairs where the key is the store’s ID and the value is the position in that store’s list.
This worked well and I duplicated this structure for the shopping lists themselves, allowing the shopping lists to be re-ordered on the main screen in addition to the items in each list.
Trouble with “orderBy” and Streams
Initially, I tried to get the items in order (for whatever store list was selected) like this:
I thought this was a very clever and the “quite obvious” approach, but the more I tested it, the more apparent it became that using “orderBy” made it so the widget(s) displaying the contents of that stream wouldn’t redraw in the UI, even if coming back from another route. It also broke the ability to cross off items: they would appear in the active and inactive lists at the same time, or they would appear in neither list, until the user reloaded that page of the app.
I went down a lot of different roads trying to fix this, but ultimately all I did was stop using .orderBy and sorted everything on the front-end (in-widget) instead. I don’t know if that’ll end up being a bad idea, but it was the only way I could get both streams and data ordered by some criteria to work together.
Sorting the Stream results
The stream-getting methods in database_manager.dart return a Stream of QuerySnapshots, like so:
And then over here in main_shopping_list.dart, I set a state variable (activeItemsStream) to the return value of that “get stream” call:
void initState() {
super.initState();
getSharedPrefs().then((storeIDFromPrefs) {
activeItemsStream = db.getActiveItemsStream(widget.list.id, storeIDFromPrefs); // should get selectedStoreID from state
inactiveItemsStream = db.getInactiveItemsStream(widget.list.id, storeIDFromPrefs);
}); // sets selectedStoreID
}
To display it, I used a custom widget that I made myself called ItemListStream that takes that state variable as a parameter (called dbStream, first one in the list) and a sortBy parameter.
That widget file is actually pretty short, here is item_list_stream.dart in its entirety. Notice the sort performed on the list items, this takes the place of “orderBy” on the database call.
I tap the “three dots in a row” in the blue “Items” bar, then I drag “Bag of potatoes” up one spot. The change is reflected on the previous screen and in Firebase.
Check out reorderable_list.dart to see how this works.
Finally, the user needed a way to change which store was selected. Here’s what I built:
I built this using a Cupertino Action Sheet. Look in main_shopping_list.dart for the full code. Changing the selected store calls _setSelectedStore, which updates the selectedStoreID state variable and “re-gets” the active items and inactive items streams with that updated ID.
This post is part of my “Building a Flutter App” dev journal, which begins here.
In this post: I develop a feature that allows the user to “link” stores and shopping lists to each other. This link is managed on the “edit store” and “edit shopping list” pages. A link is bidirectional: adding a store to a shopping list also adds that shopping list to the store.
First, the final product
The user can toggle every one of their stores on/off for each shopping list.
Feature description and purpose
This feature has a few purposes:
allow items from multiple shopping lists to appear in one store (so when you’re at, for example, “Fred Meyer”, you can see items for “groceries” and “home improvement”)
allow the user to save different item orders for different stores (this work was documented in this article)
make it easier for the user to know which store they need to go sooner to based on which items they need
Deciding how to represent the store/list links in the database
First, I consulted Firebase’s docs on structuring data to see what they had to say about representing this kind of many-to-many relationship. Their “users and groups” example is very close to what I want to build here.
They recommend using an index of groups, like so:
// An index to track Ada's memberships
{
"users": {
"alovelace": {
"name": "Ada Lovelace",
// Index Ada's groups in her profile
"groups": {
// the value here doesn't matter, just that the key exists
"techpioneers": true,
"womentechmakers": true
}
},
...
},
"groups": {
"techpioneers": {
"name": "Historical Tech Pioneers",
"members": {
"alovelace": true,
"ghopper": true,
"eclarke": true
}
},
...
}
}
Now I have a clear goal to work towards, but this should be straightforward to implement. I will have to update/maintain the link data in two places, so my Database Manager functions will account for that.
Inserting the data into the database by hand
My data differs from Firebase’s sample data in that I use the auto-generated IDs to identify my records, but I don’t show the user those IDs, I show them the name instead. I decided to change my mock data to look like this, instead:
Now the store ID are the key and store name is the value.
The next step was to put this data into my Firebase document by hand. Here is my “Back to school stuff” shopping list with two stores associated with it.
Now I’ll have something to display in the UI (and later edit).
Building the store/list linking UI
The first page I worked on was the “Edit shopping list” page. I considered doing a list of toggle switches under the “List name” field, but that list could potentially be very long and I didn’t want to push the “Save” button off screen.
Instead, I decided to show a short (truncated) list of linked stores on this page and provide a link that opens up a new view full of toggle switches that represent each possible store link.
Note: I knew that whatever UI I built for this feature would also be used by the “Edit Store” page, so I built everything using (reusable) components and named them generically.
shopping_list_form.dart now contains an internally-used method titled _formFields. This method builds a List of widgets and includes _nameField() in it by default. Whether you’re editing or creating a new shopping list, you’ll always get _nameField.
If the shopping list id is not null, then we can also show the “linked entities”.
_formFields() {
List<Widget> fields = [_nameField()];
// if we're editing an existing shopping list, add the linked stores
if (widget.shoppingList?.id != null) {
fields.add(_linkedEntities());
}
return Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: fields,
),
);
}
_linkedEntities is a separate method that returns another widget, LinkedEntitiesList.
linked_entities_list.dart is shown here in its entirety from the final version of the feature branch. The thing that’s interesting here is the use of the spread operator to build an “entity list” out of some unknown number of list elements. This turned out to be a good technique for adding a variable number of children to a column’s children array.
import 'package:flutter/material.dart';
import 'package:grocery_go/views/manage_links.dart';
import '../../db/database_manager.dart';
class LinkedEntitiesList extends StatelessWidget {
final String parentID;
final String parentName;
final String listType;
final Map linkedEntities;
final String entities;
LinkedEntitiesList(this.parentID, this.listType, this.parentName, this.linkedEntities, this.entities);
final DatabaseManager db = DatabaseManager();
@override
Widget build(BuildContext context) {
_goToManageLinks() {
var stream;
if (listType == "shopping list") {
stream = db.getStoresStream();
} else if (listType == "store") {
stream = db.getShoppingListStream();
} else {
print("Error: unrecognized list type in linked_entities_list.dart");
}
Navigator.pushNamed(context, ManageLinks.routeName, arguments: ManageLinksArguments(dbStream: stream, linkedEntities: linkedEntities, parentID: parentID, parentName: parentName, parentType: listType));
}
var _list = linkedEntities != null ? linkedEntities.values.toList() : [];
return Container(
height:300,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_listTitle(),
..._entityList(_list),
_manageLinksButton(_goToManageLinks),
],
));
}
_listTitle() {
return Text("$entities", style: TextStyle(fontSize:16, fontWeight: FontWeight.bold));
}
_entityList(list) {
const MAX_LIST_LEN = 4;
if (list == null || list?.length == 0) {
return [Text("This $listType is not attached to any $entities yet.")];
} else {
var listLen = list.length > MAX_LIST_LEN ? MAX_LIST_LEN : list.length;
var entityList = List();
for (var i = 0; i < listLen; i++) {
entityList.add(Text(list[i].toString()));
}
if (list.length > MAX_LIST_LEN) {
entityList.add(Text('+${list.length - MAX_LIST_LEN} more'));
}
return entityList;
}
}
_manageLinksButton(onPressedAction) {
return FlatButton(
onPressed: () => onPressedAction(),
child: Text("Add/Remove $entities"),
textColor:Colors.blue,
padding: EdgeInsets.all(0),
);
}
}
Notes:
I called them “entities” here because these widgets work for stores or shopping lists
I had to make a similar set of changes to the store_form.dart component
Getting a Column widget to have different children based on some condition was a new challenge. The best solution I could come up with was to use a List, called “fields” in this example, to hold the widgets that should be the column’s children. If a condition is met (in my case, shopping list id is not null), then a widget is pushed to the fields List (otherwise, it is not pushed). That approach seemed to be a good way to make the Columns children vary as needed.
user2875289‘s answer in this Stack Overflow question (method 2 and method 3 to be exact) helped me figure out how to iterate through a list to create Text widgets and how to include another Text widget child in that same array of children.
Firebase updates: adding a new store (or shopping list) to the map
Every Shopping List is going to have a map of Stores, like this:
storeRef.updateData(data, SetOptions(merge: true)); // also does not work
I kept digging and found this thread on the issue, which was last updated less than two months ago and says that merge and mergeFields are coming to the FlutterFire plugin set (which includes the cloud firestore plugin) in this update. Specifically, here are the “patch notes” for the upcoming changes to cloud firestore, which are still in review as of this writing (July 2020).
Ahh, so it seems there’s a big update coming soon that will fix this, but “merge true” is a lost cause at this point in time. If you’re reading this in the future, perhaps you have the updated cloud firestore package and none of this is a problem, but for those of us using it as it is now, I thought I’d see if I could find a workaround.
I started to wonder if I could just access shoppingLists with dot notation and gave this a try:
// working example of how to update one field in an existing map without deleting the others
Future updateShoppingListLink(String parentStoreID, String entityID, bool val) async {
DocumentReference storeRef = stores.document(parentStoreID);
storeRef.updateData({'shoppingLists.$entityID': val});
}
Yay, it worked!
This technique makes it possible to add a new field to the map without deleting the existing ones in the process.
Perhaps I didn’t need “merge true” in the first place, but I’ll leave my notes up in case they’re helpful to anyone else trying to update one entry in a map in a Firebase document.
TL;DR: “dot notation” was a good way to update specific field in a map contained within a Firebase document.
And then I changed the toggleItem method in toggle_list.dart to pass the name instead of the value:
class _ToggleListState extends State<ToggleList> {
final DatabaseManager db = DatabaseManager();
toggleItem(entityID, entityName) {
print(widget.parentType);
if (widget.parentType == "shopping list") {
db.updateStoreLink(widget.parentID, entityID, entityName);
} else if (widget.parentType == "store") {
db.updateShoppingListLink(widget.parentID, entityID, entityName);
}
}
@override
Widget build(BuildContext context) {
return ListView.builder(
shrinkWrap: true, // gives it a size
itemCount: widget.list.length,
itemBuilder: (BuildContext context, int index) {
var item = LinkedEntity(widget.list[index]);
return SwitchListTile(
title: Text(item.name),
value: widget.linkedEntities?.containsKey(item.id) ?? false,
onChanged: (bool value) => toggleItem(item.id, item.name),
);
}
);
}
}
This works – hooray! – but it reveals a new problem: what happens when the user changes the shopping list’s name?
Thoughts on what happens when a shoppingList (or Store) gets renamed:
Firebase doesn’t seem to shy away from redundant data, and the alternative seems to be to store the IDs alone and then perform a “what name goes with this ID?” look up every time the user views or manages the linked lists (or stores).
In my app, it’s probably way more common to view/manage the links than it is to rename a shopping list or store, and I don’t anticipate users having more than about 5-10 of each, so I am going to (cautiously) proceed with the idea that it’s better to update the name in multiple places if the name changes vs. the idea that the name should be looked up every time the user views a list.
I’ll revisit what happens when a shopping list or store is renamed later on in this article.
Removing an existing link from the map
Everything I’ve done so far is for creating a link.
The user can also remove a link (by toggling it to “false” in the list), which I imagined would have a database equivalent of removing the item from the map entirely. What I don’t want to do is make a copy of the entire linkings map, remove the single entry that’s going away, and then push the entire updated map.
As usual, I began with a bit of research and found that the “dot notation” that served me so well for adding a field to a map can also be used with FieldValue.delete().
Here, I’ve written a ternary that looks at the value of val. If true, it updates the ‘stores’ or ‘shoppingLists’ map with the entity’s ID and name. If false, it removes the given entity ID from ‘stores’ or ‘shoppingLists’.
In toggle_list.dart I had to make a few changes to the toggleItem method. If widget.linkedEntities is null, it creates an empty new Map(). Without this, a store (or shopping list) that doesn’t have anything in its linked shoppingLists (or stores) map will be interpreted as ‘null’, causing .containsKey to throw an exception.
Here is toggle_list.dart in its entirety.
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:grocery_go/db/database_manager.dart';
class LinkedEntity {
String id;
String name;
LinkedEntity(DocumentSnapshot document) {
this.id = document['id'];
this.name = document['name'];
}
}
class ToggleList extends StatefulWidget {
final String parentType;
final String parentID;
final List list;
Map linkedEntities;
ToggleList({Key key, @required this.parentType, @required this.parentID, @required this.list, @required this.linkedEntities});
@override
_ToggleListState createState() => _ToggleListState();
}
class _ToggleListState extends State<ToggleList> {
final DatabaseManager db = DatabaseManager();
toggleItem(entityID, entityName, value) {
if (widget.linkedEntities == null) {
widget.linkedEntities = Map();
}
// update "locally"
if (widget.linkedEntities.containsKey(entityID)) {
setState(() {
widget.linkedEntities.remove(entityID);
});
} else {
setState(() {
widget.linkedEntities[entityID] = entityName;
});
}
// update in database
if (widget.parentType == "shopping list") {
db.updateStoreLink(widget.parentID, entityID, entityName, value);
} else if (widget.parentType == "store") {
db.updateShoppingListLink(widget.parentID, entityID, entityName, value);
}
}
@override
Widget build(BuildContext context) {
return ListView.builder(
shrinkWrap: true, // gives it a size
itemCount: widget.list.length,
itemBuilder: (BuildContext context, int index) {
var item = LinkedEntity(widget.list[index]);
return SwitchListTile(
title: Text(item.name),
value: widget.linkedEntities?.containsKey(item.id) ?? false,
onChanged: (bool value) => toggleItem(item.id, item.name, value),
);
}
);
}
}
Here’s where we’re at now:
Stores and Shopping Lists have a map of their “linked entities” (stores keep track of their linked shopping lists, shopping lists keep track of their linked stores)
Linked entities can be linked/unlinked using a toggle switch
The data is changed in the database as the user toggles a link on/off
Making the link a two-way link
If the user adds the “swim stuff” list to the “Toys R Us” store, then “swim stuff” list should also get an association with the “Toys R Us” store. In other words, adding or removing a store from a shopping list should also add or remove that same shopping list from that same store.
Each existing method basically had to repeat the code of the other – once I wrote this out, I realized I could combine them into one method.
Future updateStoreLink(String shoppingListID, String storeID, String name, bool val) async {
// add a store to the specified shopping list
DocumentReference shoppingListRef = shoppingLists.document(shoppingListID);
val == true ? shoppingListRef.updateData({'stores.$storeID': name}) : shoppingListRef.updateData({'stores.$storeID': FieldValue.delete()});
// do the opposite - add this shopping list to the specified store
DocumentReference storeRef = stores.document(storeID);
val == true ? storeRef.updateData({'shoppingLists.$shoppingListID': 'temp'}) : storeRef.updateData({'shoppingLists.$shoppingListID': FieldValue.delete()});
}
Future updateShoppingListLink(String storeID, String shoppingListID, String name, bool val) async {
// add a shopping list to the specified store
DocumentReference storeRef = stores.document(storeID);
val == true ? storeRef.updateData({'shoppingLists.$shoppingListID': name}) : storeRef.updateData({'shoppingLists.$shoppingListID': FieldValue.delete()});
// do the opposite - add this store to the specified shopping list
DocumentReference shoppingListRef = shoppingLists.document(shoppingListID);
val == true ? shoppingListRef.updateData({'stores.$storeID': 'temp'}) : shoppingListRef.updateData({'stores.$storeID': FieldValue.delete()});
}
Here they are refactored into one method, with a change made to the signature to take both the shopping list name and store name.
Future updateStoreShoppingListLink(String shoppingListID, String storeID, String shoppingListName, String storeName, bool val) async {
// add this store to the specified shopping list
DocumentReference shoppingListRef = shoppingLists.document(shoppingListID);
val == true ? shoppingListRef.updateData({'stores.$storeID': storeName}) : shoppingListRef.updateData({'stores.$storeID': FieldValue.delete()});
// and add this shopping list to the specified store
DocumentReference storeRef = stores.document(storeID);
val == true ? storeRef.updateData({'shoppingLists.$shoppingListID': shoppingListName}) : storeRef.updateData({'shoppingLists.$shoppingListID': FieldValue.delete()});
}
Back in toggle_list.dart, I still have to call db.updateStoreShoppingListLink(...); in two different places because the concept of ‘parentID’ and ‘entityID’ are variable based on whether the user came in from the “edit store” flow or the “edit shopping list” flow.
When the user came in from “edit shopping list”, the parentID is a shoppingList’s ID. When the user comes in from “edit store”, the parentID is a store’s ID.
The updateStoreShoppingListLink method always expects the params in this order:
… so we change the order of widget.parentID and entityID as dictated by the list type.
// update in database
// method params: (shoppingListID, storeID, shoppingListName, storeName, value)
if (widget.parentType == "shopping list") {
// if we're editing a shopping list then the parent ID is the list ID and the entity is the store
db.updateStoreShoppingListLink(widget.parentID, entityID, widget.parentName, entityName, value);
} else if (widget.parentType == "store") {
// if we're editing a store, then the parent ID is the store ID and the entity is the shopping list
db.updateStoreShoppingListLink(entityID, widget.parentID, entityName, widget.parentName, value);
}
In this demo, toggling the “Swim stuff” list on for the “Toys R Us” store also adds the “Toys R Us” store to the “swim stuff” list.
All of these use cases work now:
Add a shopping list to a store adds that same store to that shopping list
Remove a shopping list from a store removes that same store from the shopping list
Add a store to a shopping list adds that same shopping list to that store
Remove a store from a shopping list removes that same shopping list from the store
User can remove all the shopping lists from a store
User can remove all the stores from a shopping list
Create a new store and add/remove shopping lists to it
Create a new shopping list and/remove stores to it
Handling long lists of linked entities
It’s possible that a user will add lots of stores to a shopping list (or lots of shopping lists to a store), so the list has to truncate after a to-be-determined number of items.
Already, we’re seeing some overflow after just three linked entities are present:
Currently, the logic that creates this list looks like so:
_entityList(list) {
var shortList = List();
shortList.add(Text("This $listType is not attached to any $entities yet."));
// if 'list' is empty, default to shortList which is guaranteed to have something
return list?.map((item) => Text(item.toString(), style: TextStyle(height: 1.6)))?.toList() ?? shortList;
}
The refactor needs to do the following:
display up to N (probably 4 or 5) items
append a Text widget showing count of how many items remain, ie: “+ 2 more”
still return a Text widget that says “This listType is not attached to any $entities yet” when the list is empty
Here’s what I ended up with. (There are probably more succinct ways to write this, but hopefully it’s clear what it’s doing.)
_entityList(list) {
const MAX_LIST_LEN = 4;
if (list == null || list?.length == 0) {
return [Text("This $listType is not attached to any $entities yet.")];
} else {
var listLen = list.length > MAX_LIST_LEN ? MAX_LIST_LEN : list.length;
var entityList = List();
for (var i = 0; i < listLen; i++) {
entityList.add(Text(list[i].toString()));
}
if (list.length > MAX_LIST_LEN) {
entityList.add(Text('+${list.length - MAX_LIST_LEN} more'));
}
return entityList;
}
}
The result:
Adding “store address” anywhere store names are displayed (store list, toggle list)
When I built the ToggleList and the linked entities list, I overlooked the (common) use case of the user having multiple stores with the same name. Without each store’s address on display, it’s hard to tell identically named stores apart.
However, I have a bit of a conundrum: “entities” (as they are), are just persisted to the database as a string representing the store (or shopping list’s) name. There’s no address field, nor do I really want to add one at this point.
I decided the simplest course of action would be appending the location information to the name right before it’s pushed into the database, like so: “Safeway (Kirkland)” and see how far that carries me. Is this hacky? Maybe ;) But it feels good enough for now.
In toggle_list.dart, the full list of shopping lists or stores is passed in as a Map, known as this.list:
Around line 72, each of these list items (which can be stores or shopping lists) are turned into LinkedEntity instances:
var item = LinkedEntity(widget.list[index]);
Both stores and shopping lists become instances of LinkedEntity, and they can share this “base class” because the only things used are their id and name. But now they have a third field: address. If there is an address it’s saved to the address field, but if there is no address (ie: shopping lists), it’ll just set address to be empty.
Now it’s much easier to tell same-name stores apart in the toggle list:
The same address treatment would be useful on the form page, too:
This page is trickier, because this list is taken from the saved “stores” data in the database. I considered a few different solutions, all of which felt cumbersome (on top of some already-cumbersome-feeling logic), until it dawned on me that I could just persist the “StoreName (Address)” string to the database.
It ended up being a one-line (one word, really) change in toggle_list.dart:
Now when toggleItem is called, the same “StoreName (Address)” string is passed to the database.
I’ll go with this approach for now, which seems “good enough” for the sake of this project. (I toggled each store on/off to update it to the new StoreName (Address) format.)
Two more changes…
Before moving on, I had to make two more (small) changes to the code that runs when an existing shopping list or store is updated (renamed) or when a new shopping list or store is created.
If the store already exists (it’s not null), then copy its shoppingLists into storeFields.shoppingLists and send them along to db.updateStore(...).
If the store is null, then it’s a new one being created, so create a new Map() for storeFields.shoppingLists and send that along to the db. Without this, a new shopping list has “null” for its shoppingLists map and the code that adds a new shopping list ID to it fails.
I made a similar set of changes to shopping_list_form.dart.
Updating renaming shopping lists and renaming stores to also update any saved links
The last major piece of work on this feature is making it so that updating a shopping list’s name (or a store’s name) is properly propagated to all of the documents that have it stored.
As far as I can tell, having redundancies like this (such as storing a store’s name or shopping list in multiple places) is oftentimes the preferred way of storing records in Firebase.
Per my own logic, it’s fairly uncommon to change the name of a list or a store but very common to view a store or a list in multiple places. It seemed better to take on the burden of having to update multiple records with a name change once in a rare while vs. the burden of looking up the name for every store and shopping list, by ID, every time the user viewed a list of them.
This piece of work needs to achieve the following:
When the user changes the name of a Store, step through each existing Shopping List and look for that store.id in each Shopping List’s “stores” map. If any match is found, update the name saved for that store entry.
When the user changes the name of a Shopping List, step through each existing Store and look for that shoppingList.id in each Store’s “shoppingList” map. If any match is found, update the name saved for that shopping list entry.
I started my work with renaming stores first, because stores are more complicated. Stores record their name separate from their location (also called “address” in the code), but their name and location are concatenated together when saving them into a shopping list’s list of stores.
In database_manager.dart, I confirmed that the name and address are accessible on the store DTO with a couple of print statements:
Then passed them along to a new method called updateLinkedShoppingLists:
Future updateStore(String id, StoreDTO store) async {
if (id != null && id.length > 0) {
DocumentReference docRef = stores.document(id);
Firestore.instance.runTransaction((transaction) async {
await transaction.update(docRef, store.toJson());
}).catchError((e) {
print(e.toString());
});
updateLinkedShoppingLists(store.id, store.name + " (" + store.address + ")");
} else {
print("ID is null/has no length");
}
}
Future updateLinkedShoppingLists(storeID, newName) async {
// update all the shopping lists's "stores" maps to use the new store name
await shoppingLists
.getDocuments()
.then((querySnapshot) => {
querySnapshot.documents.forEach((doc) => {
if (doc.data['stores'][storeID] != null) { // can't use ['stores.$storeID']
doc.reference.updateData({'stores.$storeID': newName})
}
})
});
}
updateLinkedShoppingLists gets all the shopping list documents from the shoppingLists collection then iterates through them. On each one, it checks if doc.data['stores'][storeID] is not null, and if it’s not null, it updates the stores entry to have the new name.
The hardest part of this process was figuring out how to only updates the stores {"storeID": "storeName"} entry for shopping lists that actually had this store in their store map. Without this logic check, every shopping list gets the store added, whether it had it before or not, but figuring out how to limit the updateData call to just the documents that had that particular store ID in its store map was a challenge. The docs didn’t really cover this scenario, and the stores.$storeID syntax didn’t work.
In other words, I couldn’t do this:
if (doc.data['stores.$storeID'] != null) { // doesn't work
It seems the handy-dandy store.$storeID lookup is only for use in the updateData({...}) call. For checking if a Firebase document had a particular entry in a map, this was the syntax that worked:
if (doc.data['stores'][storeID] != null) { ...
Perhaps because it doesn’t know the structure of ‘stores’ so it can’t use the dot notation. Either way, here is the “rename everywhere” feature working for Stores:
And getting it working for renaming shopping lists was as easy as writing the same thing again, but for shopping lists and their linked stores:
Future updateShoppingList(String id, ShoppingListDTO shoppingList) async {
if (id != null && id.length > 0) {
DocumentReference docRef = shoppingLists.document(id);
Firestore.instance.runTransaction((transaction) async {
await transaction.update(docRef, shoppingList.toJson());
}).catchError((e) {
print(e.toString());
});
updateLinkedStores(shoppingList.id, shoppingList.name); // call new method
} else {
print("ID is null/has no length");
}
}
Future updateLinkedStores(shoppingListID, newName) async {
// update all the stores' "shopping lists" maps to use the new shopping list name
await stores
.getDocuments()
.then((querySnapshot) => {
querySnapshot.documents.forEach((doc) => {
if (doc.data['shoppingLists'][shoppingListID] != null) {
doc.reference.updateData({'shoppingLists.$shoppingListID': newName})
}
})
});
}
Added a new view that lets the user toggle stores “on/off” for shopping lists (and shopping lists “on/off” for stores)
Wrote new Database Manager methods to create/delete a two-way link between stores and shopping lists whenever one is added or removed
Added a map to the store documents (and a map to the shopping list documents) that tracks the IDs and names of any linked entities
Made it so that changing the name of a store or shopping list also updates its name in any documents that have it in their linked entities map
Updated the store form and shopping list forms to display a list of linked entities, with special handling for cases where there are more than 4 linked entities
This post is part of my “Building a Flutter App” dev journal, which begins here.
In this post: I add a settings drawer and a “Dark Mode” toggle to my app. The app saves this setting locally so that the setting persists through app restarts.
First, the final product
The user toggles between dark mode and light mode in a settings drawer.
Material UI’s built in “dark” theme
First, I wanted to see what kind of “dark theme” was already supported in Flutter/Material UI. I changed the theme property to ThemeData.dark(), like so:
Now there’s a drawer with a “Dark Mode” toggle but it doesn’t do anything yet.
Toggling between light/dark mode
Still in main.dart, the next thing I did was make the theme conditional on the darkTheme variable. But wait, darkTheme isn’t available up here where MaterialApp is called. Hmm.
I decided to fix by changing the GroceryGoApp class into a StatefulWidget so I could lift the darkTheme variable up into it and check its value to determine if the theme should be ThemeData.dark() or ThemeData.light().
Then, instead of passing a title string into MainPage (which wasn’t doing anything anyway), I instead pass darkTheme and toggleTheme as parameters.
Next, in MainPage, I updated its parameters to take darkTheme and toggleTheme as passed in from GroceryGoApp. Here’s the entirety of the MainPage stateful widget and its state so you can see how the darkTheme and toggleTheme parameters come into MainPage and get passed to _MainPageState and used by the SwitchListTile.
Success! Well, sort of – the little toggle switch isn’t “toggling”. What’s up with that?
Fixing the SwitchListTile switch not toggling
As it turned out, this seemingly-small thing ended up occupying quite a bit of my time. The fix was to actually stop passing darkTheme and toggleTheme into _MainPageState the way I had been.
Instead, I just run an empty constructor and then, when I need to refer to darkTheme and toggleTheme, I address them as widget.darkTheme and widget.toggleTheme. (More on why after the code sample.)
This kinda blew my mind – I thought you had to explicitly pass parameters into the State object, but it turns out that’s not the case. In fact, you’re not supposed to pass them in via the constructor, and you should access them using widget.fieldName like I did here.
(Furthermore, any parameters that are passed to the State object through the constructor will never get updated. Lesson learned! I may have to update some of my other State objects.)
Persisting the user’s dark theme/light theme choice through app reload
Currently, the app defaults to “light mode” every time you reload the app. I’d like to persist the user’s preference, but I want to store it locally (on the device) instead of pushing it to the database.
Shared Preferences is one common solution – it’s a Flutter package that makes it easy to store key/value pairs locally on the device.
I followed this guide and made the following changes to my project’s code:
(Android Studio prompted me to update after I added this line, but you can also do the update manually via the Terminal with flutter pub get).
main.dart
import 'package:shared_preferences/shared_preferences.dart';
/* GroceryGoApp now expects a 'preferences' parameter
I pass it an instance of SharedPreferences. */
void main() async {
runApp(GroceryGoApp(preferences: await SharedPreferences.getInstance()));
}
// Here's GroceryGoApp receiving its instance of SharedPreferences in its constructor
class GroceryGoApp extends StatefulWidget {
final SharedPreferences preferences;
GroceryGoApp({Key key, @required this.preferences}) : super(key: key);
@override
_GroceryGoAppState createState() => _GroceryGoAppState();
}
/*
Hey look, a chance to use my new "widget." trick -
I don't explicitly pass preferences into State, I get them via widget.preferences...
I also replaced 'darkTheme' with a const because I like to YELL_AT_MY_CODE
(really, I just wanted to define the key name in one place in case I end up changing it, because calling it 'darktheme' in some places and 'dark mode' in others is already starting to bother me) */
class _GroceryGoAppState extends State<GroceryGoApp> {
static const DARK_THEME_KEY = 'darkTheme';
bool get darkTheme => widget.preferences.getBool(DARK_THEME_KEY) ?? false;
void toggleTheme(bool value) {
setState(() {
widget.preferences.setBool(DARK_THEME_KEY, !darkTheme);
});
}
@override
Widget build(BuildContext context) {
The first rebuild after installing SharedPreferences was pretty slow, but when it was done – ta-dah!
Oh, no, what’s this? A bunch of error output:
Launching lib/main.dart on iPhone SE (2nd generation) in debug mode...
Running Xcode build...
Xcode build done. 22.9s
path: satisfied (Path is satisfied), interface: en0
Configuring the default Firebase app...
Configured the default Firebase app __FIRAPP_DEFAULT.
path: satisfied (Path is satisfied), interface: en0
path: satisfied (Path is satisfied), interface: en0
[VERBOSE-2:ui_dart_state.cc(157)] Unhandled Exception: ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized.
If you're running an application and need to access the binary messenger before `runApp()` has been called (for example, during plugin initialization), then you need to explicitly call the `WidgetsFlutterBinding.ensureInitialized()` first.
If you're running a test, you can call the `TestWidgetsFlutterBinding.ensureInitialized()` as the first line in your test's `main()` method to initialize the binding.
#0 defaultBinaryMessenger.<anonymous closure> (package:flutter/src/services/binary_messenger.dart:76:7)
#1 defaultBinaryMessenger (package:flutter/src/services/binary_messenger.dart:89:4)
#2 MethodChannel.binaryMessenger (package:flutter/src/services/platform_channel.dart:140:62)
#3 MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:314:35)
#4 MethodChannel.invokeMapMethod (package:flutter/src/services/platfo<…>
Debug service listening on ws://127.0.0.1:64936/RkSpLN3ICpY=/ws
Syncing files to device iPhone SE (2nd generation)...
This seems to happen because the app is waiting on main to do something (main is now async, so that makes sense), and the fix is to add WidgetsFlutterBinding.ensureInitialized() to the first line of main.
With SharedPreferences in place, I can now toggle the app to “dark mode”, close the simulator, re-build, and the app is still in “dark mode” when I reopen the app. Yay!
This post is part of my “Building a Flutter App” dev journal, which begins here.
In this post: I built the “cross off” feature to my “Grocery Go” shopping list management app.
First, the final product
Watch as items move between the top list (the “active” list) and the bottom list (the “crossed off” list).
Feature list
Here are all the things that should happen when this feature is considered done:
When the user taps the item, it is visibly removed from the shopping list and added to the “Crossed off” list below the shopping list
The parent shopping list itemCount is decreased by one
A “crossed off” item appears at the top of the crossed off items list
The time/date of this action is saved to the item so we know when it was crossed off [dateLastMoved]
If the user taps a “crossed off” item, it returns to the shopping list and the itemCount is increased by one
The time/date of this action is saved to the item so we know when it was added to the list [dateLastMoved]
Shopping Lists maintain their own lists of Crossed Off items, so that the user doesn’t see a crossed off item from their “home improvement” list in their “groceries” list – add CrossedOff subcollection to Shopping List
Adding the isCrossedOff property to Item
I thought it might be simple to just have each item track whether it is “active” or “crossed off”. isCrossedOff will be a boolean value on Item instances, so it had to be added to any code that models Item’s data.
To avoid having to delete my existing items and re-create them through the form just now, I also went into their Firebase documents and added isCrossedOff to my three test items:
Filtering the item stream by isCrossedOff’s value
Next up: adding a second “get item stream” method to database_manager.dart.
Originally, getItemsStream() returned all the items. Now, I want it to only return items where isCrossedOff is set to false, like so:
Now the items are being filtered by their isCrossedOff value:
Once I saw it working and was satisfied that I could filter the items this way, I decided to do a quick refactor to turn getItemsStream() in database_manager.dart into one method that takes a second parameter indicating whether I want the active items or the crossed off items.
Moving items between the “active” list and the “crossed off” list
In main_shopping_list.dart, there are two methods that get passed all the way down to the Item instances:
_crossOff(Item item) {
print("Remove this id from this list: " + item.id);
}
_addToList(Item item) {
print("Moving this item back to the list: " + item.id);
}
They are fed into the ItemListStream as onTap: methods. Items that are ‘active’ get the _crossOff method:
Note: I initially set out to use the “ItemDTO” for this update, but I couldn’t figure out how to use it “partially” – ie, the only parts of the item I want to update are the lastUpdated and isCrossedOff fields, but the ItemDTO requires all of the fields to be present. I could copy the entire item into it, but I wonder if there’s just some better way to do this in general…
Welcome to Part 6 of my Flutter app dev journal. Now that the Firebase connection is working, it’s time to add some “create”, “read”, and “update” functionality.
Full disclosure: I’m saving “delete” for later. This is just the “CRU” part of “CRUD”.
Seeding the database with the first bit of data
I decided to start with “shopping lists”, so the first collection I created in Firebase was shopping_lists and I gave it one document. Now there’s something to get from the database, and a collection to add to when new ones are made.
Note: I made an id field and copied the document’s ID into it so that the ID would be easily accessible on the record’s object.
Creating a database manager class
The DatabaseManager class is a singleton that’ll get imported into any file that needs to interact with the database. In my project, it is located in lib/db/database_manager.dart
This is the absolute simplest thing I could think of – all this does is get the shopping list records as a stream. I modeled it (loosely) on the examples found in this helpful tutorial.
database_manager.dart
import 'package:cloud_firestore/cloud_firestore.dart';
class DatabaseManager {
final CollectionReference shoppingLists = Firestore.instance.collection('shopping_lists');
Stream<QuerySnapshot> getShoppingListStream() {
return shoppingLists.snapshots();
}
}
Using the database manager to get records from the Firebase database
I went back to main.dart and imported the DatabaseManager.
import './db/database_manager.dart';
Then, in the “return”, I added reference to a _shoppingLists widget:
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
ItemListHeader(text: headerShoppingLists),
_shoppingLists(context), <--- this is what's new
And then I created the new _shoppingLists widget, still in main.dart. It uses the StreamBuilder to get records (which may come in piecemeal from the database) and either display them as an ItemList or display an error.
This is when my database permissions error became obvious, as no data was actually coming in from the db even though I was ready to display it in the Flutter app.
Troubleshooting Firebase access denied (“ConnectionState.waiting” always being true)
When I tried to get my data from the db, I got a “Missing or insufficient permissions” error.
I checked the value of snapshot.connectionState and found that it was equal to ConnectionState.waiting all the time.
Widget _shoppingLists(BuildContext context) {
return StreamBuilder(
stream: db.getShoppingListStream(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("waiting is true!");
}
...
The quick fix is to make read/write open to anyone.
By default, the rules are:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
I changed them to:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
This is obviously a bad idea in the long term, but I should be able to change the rules to allow registered, authorized users access to their own records (and deny everyone else) and I plan to build that soon, so for now this is acceptable.
Here’s where we’re at now:
“Hardware store” and “Groceries” are from the database, so this is forward progress even if we lost the “Stores” list in the process.
“Stores” broke because the ItemList widget expects to be fed a document snapshot, and I am going to fix that next.
This is mostly a repeat of the steps I just did to make shopping_lists. First, I added a stores collection and populated it with one store…
And then I added a way to retrieve the store record(s) in database_manager.dart:
import 'package:cloud_firestore/cloud_firestore.dart';
class DatabaseManager {
final CollectionReference shoppingLists = Firestore.instance.collection('shopping_lists');
final CollectionReference stores = Firestore.instance.collection('stores');
Stream<QuerySnapshot> getShoppingListStream() {
return shoppingLists.snapshots();
}
Stream<QuerySnapshot> getStoresStream() {
return stores.snapshots();
}
}
And finally, create a _stores widget in main.dart that does the same thing _shoppingLists does:
Now we have stores coming in from the database, too. Yay!
Adding new shopping lists and stores via the in-app forms
Creating the data manually in Firestore’s database dashboard is no fun, so it’s time to hook up the in-app forms to the real database.
I am going to begin by adding the “create a new shopping list” feature.
Data transfer object
For interactions with the database I like to use what’s called a “data transfer object”, it’s just a way of making sure the data sent to the db confirms to a certain structure. I created a new file, shopping_list_dto.dart and built it out as so:
The “toJson” method will be useful when we need to format the data for insertion into the database.
Next, I modified new_shopping_list.dart:
import 'package:flutter/material.dart';
import 'package:grocery_go/db/database_manager.dart';
import 'package:grocery_go/db/shopping_list_dto.dart';
class NewShoppingList extends StatefulWidget {
static const routeName = '/newShoppingList';
NewShoppingList({Key key});
@override
_NewShoppingListState createState() => _NewShoppingListState();
}
class _NewShoppingListState extends State<NewShoppingList> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Add new shopping list"),
),
body: Center(
child: Padding(
padding: EdgeInsets.all(20),
child: AddShoppingListForm(),
),
),
);
}
}
class AddShoppingListForm extends StatefulWidget {
@override
_AddShoppingListFormState createState() => _AddShoppingListFormState();
}
class _AddShoppingListFormState extends State<AddShoppingListForm> {
final formKey = GlobalKey<FormState>();
final DatabaseManager db = DatabaseManager();
final newShoppingListFields = ShoppingListDTO();
String validateStringInput(String value) {
if (value.isEmpty) {
return 'Please enter a name';
} else return null;
}
void saveNewList(BuildContext context) {
final formState = formKey.currentState;
if (formState.validate()) {
// save the form
formKey.currentState.save();
// this data is auto-generated when a new list is made
newShoppingListFields.date = DateTime.now().toString();
newShoppingListFields.itemIDs = new List<String>();
// put this stuff in the db
db.addShoppingList(newShoppingListFields);
// confirm with a snack bar
Scaffold.of(context).showSnackBar(
SnackBar(content: Text('New list created: ' + newShoppingListFields.name))
);
// go back to main view
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
return Form(
key: formKey,
child: Column(
children: [
TextFormField(
autofocus: true,
decoration: InputDecoration(
labelText: 'Shopping list name',
border: OutlineInputBorder()
),
validator: (value) => validateStringInput(value),
onSaved: (value) {
newShoppingListFields.name = value;
}
),
RaisedButton(
onPressed: () => saveNewList(context),
child: Text('Save'),
),
],
),
);
}
}
What’s new:
Changed the “form” into an actual Form widget so it can behave like a proper Flutter form
Instantiates an instance of ShoppingListDTO called newShoppingListFields and fills it out using form data
Added a form key (which is how Flutter distinguishes forms from each other)
Added validator property to TextFormField and created a simple validation method (all it does is check that there’s any input at all)
Added onSaved property to TextFormField so it knows what to do when the form is saved
Added saveNewList method that runs validation and, if valid, sends this form data off to the db and returns the user to the main screen
Changed the way the date is generated, it’s now converted to a string before it goes to the db
Getting this all working took a bit of trial and error. I experimented with Timestamp and DateTime objects before settling on pushing the date to the db as a string. I also had to try a few things before I figured out how to push the empty itemIDs array into the db in a way that would be recognized (on retrieval) as having a length.
Here it is! Now the user can create a new list and see it on the main screen.
And here it is in the database:
Just one thing is missing: the form-created shopping list needs to have its “id” field filled in after the record is created, so that’s next up.
Grabbing the new record’s ID and saving it to the record
I’d like every document (so every item, every shopping list, every store, etc.) to store its own ID in a data field. This should be useful for editing entries later on.
I wasn’t completely sure how to approach this at first – there is no ID until the document is created, so I have to created something in order to get that ID back.
This lets me grab ID that comes back and immediately update the document to have that ID, like so (in new_shopping_list.dart):
void saveNewList(BuildContext context) async {
final formState = formKey.currentState;
if (formState.validate()) {
// save the form
formKey.currentState.save();
// this data is auto-generated when a new list is made
newShoppingListFields.date = DateTime.now().toString();
newShoppingListFields.itemIDs = new List<String>();
// put this stuff in the db and get the ID that was created
DocumentReference docRef = await db.addShoppingList(newShoppingListFields);
// update the record to have the ID that was generated
newShoppingListFields.id = docRef.documentID;
db.updateShoppingList(docRef.documentID, newShoppingListFields);
// confirm it with a snack bar
Scaffold.of(context).showSnackBar(
SnackBar(content: Text('New list created: ' + newShoppingListFields.name))
);
// go back to main view
Navigator.of(context).pop();
}
}
Now the document’s ID is duplicated into a field on that document:
Refactoring it a bit…
This works, but I don’t like that my DatabaseManager is basically foisting this work onto whatever code is calling it. We’re never going to create a document and then not immediately turn around and slap the ID into it, so I wanted to see if I could put encapsulate this work within database_manager.dart
Initially, I ran into trouble trying to create a DocumentReference – a Future<DocumentReference> is not a DocumentReference, it seems.
This might be a job for async/await, which I’ve used in JavaScript/TypeScript but have not yet attempted in Flutter/Dart, so here we go – now it’s async/awaited and returning that DocumentReference.
And then over here in new_shopping_list.dart, I removed the “update the ID” code:
void saveNewList(BuildContext context) async {
final formState = formKey.currentState;
if (formState.validate()) {
// save the form
formKey.currentState.save();
// this data is auto-generated when a new list is made
newShoppingListFields.date = DateTime.now().toString();
newShoppingListFields.itemIDs = new List<String>();
// put this stuff in the db
var docRef = await db.addShoppingList(newShoppingListFields);
print("Created record: " + docRef.documentID);
// confirm it with a snack bar
Scaffold.of(context).showSnackBar(
SnackBar(content: Text('New list created: ' + newShoppingListFields.name))
);
// go back to main view
Navigator.of(context).pop();
}
}
(I later cleaned it up by removing var docRef = and the print statement, I just wanted those to confirm that everything was working the way I expected.)
And there we have it – now the work of updating the ID is done by the DatabaseManager, which I think is just a better design for this particular use case.
All of the code pertaining to hooking up to Firebase and getting creation and retrieval working can be found in this pull request. (Hey, we’re halfway to a CRUD app!)
Sorting the shopping lists by name (alphabetically) as they come in from Firebase
Before we move on and Firebase-ify the rest of the app, I want to fix the way shopping lists appear in a seemingly random (or at least unpredictable) order.
In the long run, it’d be nice if the user could re-order these lists, but for now, I think I’ll sort them alphabetically.
Firebase has .orderBy, but it took me a bit of trial and error to realize I had to apply it to the collectionReference, not the part where we get that reference in the first place (so not the Firestore.instance.collection("collectionName") part.
Like this:
class DatabaseManager {
final CollectionReference shoppingLists = Firestore.instance.collection('shopping_lists');
final CollectionReference stores = Firestore.instance.collection('stores');
Stream<QuerySnapshot> getShoppingListStream() {
return shoppingLists.orderBy("name").snapshots();
}
Stream<QuerySnapshot> getStoresStream() {
return stores.orderBy("name").snapshots();
}
...
Which results in the shopping lists being sorted by name:
Minor thing, but it was bothering me the way new ones didn’t seem to have any rhyme or reason to where they ended up in the list.
Hooking up the rest of the app to the database
Making the rest of the app work with Firebase was a decent amount of work, and most of it was a re-hash of what was already done above, but I still broke the major steps into individual pull requests for anyone interested in seeing them.
Working with Items (items being things like “eggs”, “bread”, etc.) made it apparent that they should be stored as a subcollection of a Shopping List, rather than have their IDs saved in an array on shopping list and retrieved separately.
Firebase seems to prefer you just stick the child document(s) inside their parent documents, rather than maintain a list of child document IDs the way I’m used to doing with MySQL databases.
In Firebase, the shopping list’s ‘items’ subcollection looks like this:
To create an item and add it to the subcollection, database_manager.dart now has the following method:
Gets the parent shopping list document by its ID and updates “itemCount” (since we don’t have the itemIDs array to get the length of anymore)
Gets the parent shopping list document by its ID, gets the collection of ‘items’ within, and adds the new Item (as JSON) to that subcollection of items
Immediately gives that new Item its own ID as a field called ‘id’
The app now uses real data from the Firebase db for its shopping lists, items, and stores!
At this point the app has the most basic “create”, “read”, and “update” support for shopping lists, stores, and items, but the app needs a whole bunch of feature love to feel more polished.
Join me in Part 7 [Coming soon] as I add a bunch of new features.
In this article: my review of Oregon State University’s online post-bacc CS degree program, which I attended for 3.5 years, starting in September 2016 and finishing in June 2020, completing 15 courses with a 3.9 GPA and graduating with my second bachelor’s degree, a Bachelor of Science in Computer Science.
Yay! Being done is a great feeling. Pictured: my new diploma surrounded by the books, notes, and worksheets that were my BFFs during this program.
First, I’ll answer the most frequently asked questions I was asked while in the program:
Yes, the program is completely online – the coursework is unlocked weekly in the online portal known as “Canvas”, you take tests online with a proctor supervising you over webcam the whole time, you submit coursework and get your grades online
No, it is not self-paced – quarters are 10 weeks long, deadlines are firm barring exceptional circumstances (I never pursued an extension but they exist for those who can justify the need for one)
But you can vary your pace through the program, such as taking 1 class a quarter to go at a slower pace and leave more time for the rest of your life, or taking 2-3 classes a quarter to get through the program faster (more thoughts on class pairings later in this article)
You can take quarters off – I took a few quarters off here and there for things such as moving cross country and having a baby
Yes, you have to already have a bachelor’s in some unrelated field – it’s a “post-bacc” program, so your first degree entitles you to skip all the gen-ed classes you’d normally need to do
It’s still a normal bachelor’s degree on paper and the degree does not call out the “online” nature of the program in any way (you can see the diploma I received at the top of this article)
Yes, there is a decent degree of “self teaching” in the program – at a high level, the courses give you problems to solve and they usually provide lectures and readings to guide your learning, but you’ll be on YouTube, Stack Overflow, experimenting in your own codebase, etc. to find answers and a deeper understanding than the course materials alone give you.
Yes, it was still challenging (and I learned a lot) even with prior industry experience. I came into this degree with about 2.5 years of web development experience and prior to that, a bootcamp, and I still learned a ton from most classes. Previous experience will give you a small advantage, but there’s still a ton of work to do and problems to solve that take time and effort.
Yes, it requires sacrifice – there are many things I missed out on during the last four years because I was busy with school, which dominated evenings and weekends.
Yes, it was an excellent experience – I learned so much and rounded out my CS knowledge in ways that my bootcamp and industry experience had not.
Computers have been my passion since I was a child, and completing this degree meant a lot to me. It formalized something I’ve explored via hobby projects, game mods, my own little websites, etc. since I was a kid.
My favorite resource for learning more about the program is r/OSUOnlineCS. I lurked here for nearly a year before I applied, and visited frequently while in the program.
About me (and some encouragement for other “non-traditional” students)
I want to write briefly about being a “non-traditional” student because that’s basically who this program is for: students who already have an established life and commitments outside of school.
Returning students usually have at least a couple reasons why they think they can’t go back to school – they’re “old” (over 30, or 40, or whatever), they have children, they have a full-time job, etc.
I check all of those boxes – I’m almost 37, I had two babies while I was in the program, I worked full-time for part of it, I relocated cross-country, blah blah. If this is you, with all these existing demands on your time, I just want to say: it is possible! It is hard but it is possible.
I met quite a few other parents with full-time jobs on my journey through the program so I don’t think I am unique, just disciplined and willing to work hard. If this is you, too, don’t count yourself out just because you have other things going on in your life.
My path through the program
Here are the classes I took in the order I took them, along with links to my reviews on each class.
OSU regularly updates and revamps the courses, and some of these courses were revamped since I took them.
The most notable (and controversial) change was the switch from C/C++ to Python in the foundational classes. I took the C/C++ versions of 161, 162, and 261.
I think this was a good change. While I appreciate the experience I got with pointers, memory allocation, and a relatively “low-ish level” language, sometimes it felt like debugging C got in the way of learning the higher level concepts the courses were trying to teach.
What to expect from OSU’s online CS program
Pacing and scheduling
Classes follow a strict schedule. Each week, usually on a Sunday, a batch of lectures, worksheets, projects, quizzes, discussions, etc. unlocks. Most classes have a “late work” policy you can invoke (once or twice) if you need an extra day or two, but this is not a self-paced program. Deadlines and exam windows are generally strict.
The workload and time commitment varies a lot between classes. The OSU CS Course Explorer is a tool I made to help students determine which classes to pair and which to take alone. It scrapes its data from the survey hosted on r/OSUonlineCS. You can also search the OSUonlineCS subreddit for tips and advice from other students.
At any given time in the program I was either working full-time with a baby, or caring for two children while working my way through the courses. I frequently alternated between one class a quarter and two classes a quarter.
When I took one class at a time, I felt like I was making glacial progress towards the degree but I also had room to breathe. I could dig into each assignment, do extra credit, work on side projects, or spare a few days for an illness or a short trip somewhere. I even went to Disney World for three days in the midst of CS225! :)
When I took two classes at a time, every week was the time management Olympics with no room for slop. I started assignments the moment they unlocked and made every minute count, but the sheer workload guaranteed the two-class quarters were brutal. I lost my 4.0 GPA the first time I attempted two classes at the same time. It was exhausting, but… faster progress!
Classes to take alone
I realize there are financial aid and “I want to get this over with” reasons to take two (or more) classes at a time, but my personal recommendation is that these classes benefit from being taken alone due to their difficulty, time-consuming nature, and/or overall importance to one’s CS education:
CS162 Intro to Software Engineering II – insane workload and challenging topics, but I took the C version so maybe it’s not quite so unforgiving now
CS325 Algorithms – most challenging course of the program, it dominated my life for 10 weeks
CS261 Data Structures – high workload and challenging, especially when it was taught in C
CS271 Computer Architecture and Assembly – high workload and challenging subject matter
CS467 Capstone – it’s your final project, give it (and your group) the time it deserves
Classes to take with other classes
These classes were lighter weight or just generally not so programming heavy. A coding problem can occupy me up for hours as I investigate and trial-and-error my way through it, but I never get stuck like that when the task is just to write a document or forum replies.
CS225 Discrete Math – challenging but manageable
CS361 Intro to Software Engineering I – mostly a writing class
CS290 Web development – relatively light workload
CS340 Database – a very entry-level look at SQL plus a final project you can do the bare minimum on if you are so inclined
CS3344 Operating Systems – the projects are huge but they’re manageable, topics aren’t exceptionally challenging
Obviously, your mileage may vary – and courses get revamped every so often, too, so my opinions and experiences will become less relevant over time. (This was written summer 2020.)
Exams
Most classes have some sort of big test to take at some point. Some classes split this up as a “midterm” and a “final”, some require it to be proctored (supervised), some allow open book, and a few don’t have any kind of big tests at all. At one end of the grading spectrum are classes that treat exams as the bulk of your final grade, while others weight their exams so lightly you can basically fail them. Every class is a little different in this regard.
Before doing this degree, I was never the kind of person who studied for exams. I usually just winged it and did okay, but the OSU classes demanded it. Flash cards, practice worksheets, practice exams – I did them all, and usually managed a B or better on midterms and finals.
Proctored exams are no fun but they help ensure the quality of the program’s graduates. A proctored exam is taken either at a local test center (if you live near a community college they may offer proctoring services) or online with a service such as ProctorU on your usual computer. The overwhelming majority of students seemed to be using ProctorU.
For the first half of the program I took my exams in person at a community college branded testing site near me. The big advantages with an in-person testing site:
a dedicated testing environment where no one is going to come along and ring your doorbell mid-exam
you get to use scratch paper instead of a whiteboard
ProctorU makes you use a whiteboard, and it’s small so you have to have to wipe it every few problems and can’t easily refer back to previous problems later in the exam. With a stack of scratch paper, you don’t “lose” your work as you work through the exam.
But, after a scheduling snafu with my local test site, I switched to ProctorU and never went back to my local site. ProctorU was half the cost of the test center and way more flexible with the scheduling options (my test center was only open a few days a week and after they lost my reservation and had no proctor present on the last day of the exam window, I was done with them).
As a whole, I would describe the exams in this program as tough but fair. There are a few exceptions, of course – CS290’s final stands out as being particularly bizarre and nonsensical, but it made up for it by being worth just a small portion of the final grade. Most classes seemed to derive around 20-50% of the final grade from tests alone.
Course content and materials
Unfortunately, as a whole, the course lectures are not great. There are some exceptions (CS492 had amazing lectures), but in general, you can find better instruction on virtually every topic covered by this degree program on YouTube, and you should. There are tons of talented people out there who can teach you how to do a summation or how to step through recursion better than the OSU course materials.
Courses get refreshed every 3 years on a rolling basis (so not all at the same time), and you’ll see people in the Slack channels and the subreddit talking about upcoming refreshes and retooling their schedule to get in either before or after a refresh. In my experience, the refreshes tend to be a good thing – you want to get in after a refresh, rather than before.
The most notable refresh that occurred while I was in the program was the switch from C++ to Python in CS161 and CS162 (with the change propagating up to the higher level classes after I graduated). There was also an elective that got split into two separate courses (Mobile & Cloud became Mobile and Cloud). Also, CS372 became an elective, and new electives were added.
I think these are good changes – they are trying to keep the courses relevant to the industry as a whole and useful to students. When I was in it, CS162 was like being punched in the face every week, but in the three quarters since that change occurred I’ve heard much better things about the class.
Feedback and help
You will get little to no feedback on the code you turn in. I never got any kind of “Hey, you know what might make this code read better…” input on any of the code I wrote for this program. Do not enroll in this program hoping to get 1:1 feedback on your code, because it will not happen. (I did get quite a bit of that kind of help at my bootcamp, though.) This was a disappointment for me in the program. I was accustomed to code reviews on everything I wrote at work so just turning in whatever I wanted and never hearing a word about whether the code style or approach was any good was a weird feeling.
Office hours – most courses had a scheduled set of times each week where you could drop into the Slack chat or a meeting room and converse with one or more TAs about the coursework. I rarely attended these – they were almost always at some impossible time (either they overlapped my work hours or they were right in the middle of the dinner hour) or way too late in the week (I was not going to “save” my problems for a Friday TA chat when the thing is due Sunday).
There’s a Slack channel for every class but the usefulness of each one varied widely. Some were great, with active discussion and helpful links and tips being shared. Others were completely dead.
As I was finishing up at OSU they were moving to a system of having official Slack channels for each class but having a teacher and/or TA in the room seemed to kill any interesting discussion beyond simple Q&A about the assignments.
Other student projects on GitHub/blogs – these were a goldmine of help. Usually the assignment had changed in some way since someone posted their code five years back, but having something similar to analyze was extremely valuable to me. Some OSU assignments are identical to assignments given by other universities, too, so if you find yourself stuck on an assignment just searching for the premise of the assignment might uncover something helpful.
Group work
There is a lot of group work in this degree program and for the most part it sucks.
The group work in this program is at its best when it’s things like discussions on Canvas or Piazza, and it’s at its worst when you’re a team of 3+ and one (or more) of your team members isn’t doing anything at all.
The problem with group work in an academic setting is there’s no recourse for problem teammates. In a professional environment, the team usually has avenues for dealing with a non-performing or always-absent teammate. Generally, managers don’t let someone do nothing and collect a paycheck. In a classroom setting, though, these non-performers are your problem.
I cannot sugarcoat it: many group projects in this program will feature one or more students who just don’t give a shit. I had group members who promised to do work and then just never did it, group members who turned in garbage, group members who skipped meetings and acted like it was no big deal if they turned their part in the same day it was due while others in the group were unable to work until that part was done. I had one group member drop a mess into the codebase and then go out to dinnerfor two hours just hours before it was due.
The TAs and teachers don’t want to hear about your team’s turd, so it is best to make peace with it and come up with contingency plans for your group’s non-performers. Try to arrange the workload to minimize dependencies – ie: don’t make it so you can’t start your part until someone else has finished theirs, because that person might wait until the 11th hour to do the part you’re waiting on. I’ve talked with enough other students to know that my experience was not unusual. Expect some dead weight on at least a few of your group projects. The group work is one of the worst parts of the OSU program.
Why I enrolled in OSU’s CS program even though I’m a web dev bootcamp grad who already works in software
My first degree is in fine art and my first career was in designing video games. The industry shift to “free to play” style games left me unhappy with my role. I’d always liked making websites, so in 2014 I quit my video game designer job and completed an 8-week web development bootcamp with Code Fellows in Seattle, WA.
My first engineer role was at Expedia, where I worked on a web app that allowed Expedia’s suppliers (people selling tours and activities) to list their products on the flagship website. This was great – I loved the bootcamp and I loved my job at Expedia, but I felt like my long-term advancement might be held back by the lack of a degree. (Some people even made comments to me to that effect, too.)
With decades of career still ahead of me, I decided to invest in a degree to support it.
I was impressed at how many of the topics I first encountered in my bootcamp resurfaced while working on this degree. Recursion, picking the right data structure for the task, interfacing with an API, CRUD operations, writing tests, and performance considerations were all covered in the bootcamp. Doing this degree made me feel even better about how good my bootcamp was! (Thanks, Code Fellows! You guys are legit!)
To me, this CS degree symbolizes my commitment to my new career and dedication to learning. I was already tooling around with side projects all the time, why not focus them into something tangible?
People argue that you can learn all of this stuff online for free, and that is true – I relied heavily on free online resources for every class in this degree – but the program exposed me to (and pushed me through) to things I’m not even sure I would have found on my own. For example, I don’t think I would have built a shell in C or worked through recursive algorithms on my own. I definitely would not have studied discrete math on my own. And I probably would’ve given up on some of the harder things if it weren’t for needing to pass a class motivating me to work through it.
People say you don’t need a degree once you’re already working, but I was called “just a boot-camper” or “just a front-end dev” enough times to convince me otherwise.
With the degree, now I feel like I can do anything!
Job prospects
I was going to fill out this section with a brief discussion of my post-degree job search, but due to COVID-19 I’m in a holding pattern as I wait for my children’s daycare to reopen and for it to be safe to venture into the outside world again. Many companies seem to be on hiring freeze, too.
I’ll write something useful here when I have something to report.
In conclusion
To some extent, it’s hard to say if the degree is worth it when I’m only a month post-graduation and have barely begun my job search (in the midst of COVID-19, no less).
But I can say this: it was a good 4 years and I’m proud of the work I did.
The degree was no replacement for on-the-job training: the degree didn’t touch 90% of the technologies, techniques, workflows, or complexities of the codebase that I encountered in my professional job. The bootcamp I did in 2014 handled those areas better.
Here’s a diagram that attempts to illustrate how much “overlap” there was in topics and learning between my bootcamp, the OSU degree, and the web developer job I had for the first three years after I finished the bootcamp:
This diagram needs a big fourth circle labeled “tinkering around with stuff on my own”.
Just going by what my other CS-degree having friends have told me about my coursework and projects vs. theirs, the OSU program seems to be roughly on par with what other state schools are offering for on-campus CS. In general, the OSU program seems to be more modern than what some of my friends did for their degrees (circa 2005-2015), though less math-heavy.
Overall, the degree gave me what I wanted: the peace of mind that comes from having put in the hours and the work to earn the same degree so many of my colleagues have. I think the degree will open doors down the road, especially if I ever want to do something besides front-end web dev (which I’m actually very happy in). Hopefully, the degree communicates that I am serious about my career change and dedication to CS!
I’m glad I did it.
Be sure to check out my individual course reviews if you want to know more about the classes in greater detail.
In this post: Adding Firebase to my Flutter app. This is mostly setup (there was a fair amount of it) and the actual coding resumes in Part 6.
At this point, passing around the mock data objects is getting to be more of a nuisance than a help. I don’t want to write code just to support the passing-around of mock data, so it’s time to start putting that data into the db and retrieving it.
But there were still a few places where I felt my experience deviated from the videos/docs or where I just kinda got lost, so I am documenting them here.
Registering the app with iOS
The Firebase instructions assume you’re working in Xcode, but if you’re like me and working with a different IDE (Android Studio) in my case you might be wondering how to get the iOS bundle ID.
Here’s what I did (and as far as I can tell, this step has to be done in Xcode.)
Open Xcode
Go to “File… Open” and open just the iOS directory of my project
3. Click on the “Runner” header in the project structure at left and retrieve the “Bundle identifier” from the information at right (just copy/paste it and leave XCode open because we come back to it in a later step).
4. Give that information to Firebase and click Register App
5. Download the GoogleService-Info.plist file it generates for you
6. Note that they tell you this file belongs in the “root of your Xcode” project. (The Flutter/Firebase video totally skips this step, too!) It took me some trial and error to discover that the right way to “install” this file is to drag it from Finder to the Runner folder in Xcode.
Do not do this operation just in Finder (or via Terminal), you must do this step in Xcode.
Make sure “Copy items if needed” is checked
Make sure “Add to targets Runner” is checked
If it looks like this, you’re done and can close Xcode:
7. Since this file contains things like my API key, I added *.plist to my .gitignore file (so you won’t see it if you browse the project there).
8. Back in Firebase setup, I skipped this step:
9. I also skipped the “Add initialization code” step which is for native projects.
Registering the app for Android
The Flutter/Firebase video covers this, but it happens so fast I had to watch it like 5 times to figure out where, exactly, this file is (and of course, pausing the video brings up a bunch of YouTube links and “related video” junk that covers the code, lol).
Search for and open AndroidManifest.xml:
The line in question is at the top, and the highlighted line is the part you need:
I do not actually have an Android device so for now I just have to hope this works, but I won’t know until later in development. I’ll come back and update this section if I have trouble using Firebase on an Android device.
Creating the Firebase database
After the project was successfully created on Google’s end, I created a new database and went with the default settings for now.
Note: I later had to change the “false” to “true”, because by default the Firebase database doesn’t allow access to anyone.
Now, return to Terminal and run flutter pub get (or if you are in Android Studio it might prompt you to update dependencies as soon as you flip over to another file).
Rebuilding the app
Finally, stop the app if you have it running and rebuild (hit the green “Play” button in Android Studio). Just a heads up, this particular build took a lot longer than they normally do.
Continue on to Part 6 where I write some basic create, read, and update operations for the app.