Working with Firebase Realtime Database
In the previous post, we set up authentication. Now that the user is authenticated, we can store and retrieve data from the database specific to that user.
Modeling the data
The Firebase Database documentation explains how to model data in a way that avoids data duplication. It's called data normalization, which is a familiar concept in SQL. In NoSQL, they suggest flatter hierarchies, and using keys as references.
That is what I will do for named locations. Home
will have a geolocation associated to it, and any reminder that is intended to surface at home will simply refer to it. I also added an example of hierarchies of reminders with the children
and parent
fields.
One other thing to note is that arrays are actually key/value objects.
{
user1: {
reminders: {
one: {
label: "deposit check",
triggers: {
// when passing by the bank
one: {
type: "location",
location: ...,
radius: 3
},
two: {
type: "dayOfWeek"
dayOfWeek: {
Saturday: true,
Sunday: true
}
}
}
},
two: {
label: "practice piano",
triggers: {
one: {
type: "namedLocation",
locationId: "home"
}
}
},
three: {
label: "read book",
triggers: {
one: {
type: "namedLocation",
locationId: "home"
}
}
},
xxx: {
label: "todo app",
triggers: {
one: {
type: "namedLocation",
locationId: "home"
}
},
children: {
xxx1: true,
xxx2: true
}
}
xxx1: {
label: "new feature1",
parent: "xxx",
children: {
xxx1_1: true,
...
}
},
xxx2: {
label: "new feature2",
parent: "xxx"
},
four: {
label: "get off bus",
triggers: {
one: {
type: "namedLocation"
locationId: "home"
}
},
triggerWhen: "always"
}
},
namedLocations: {
one: {
label: "home"
location: ...,
radius: 12
}
}
},
user2: {
...
}
}
Writing data
First write
I used the Firebase documentation again. To add a reminder into the list of reminders, you first get a key to a new reminder, then set its value:
lateinit var mDatabase: DatabaseReference
lateinit var userId: String
...
override fun onCreate(savedInstanceState: Bundle?) {
...
userId = FirebaseAuth.getInstance().currentUser!!.uid
mDatabase = FirebaseDatabase.getInstance().reference
...
}
fun addReminder() {
val userReminders = mDatabase.child("users").child(userId).child("reminders")
val key = userReminders.push().key
val reminder = Reminder("buy groceries")
userReminders.child(key).setValue(reminder)
}
Oh and I had to read through the Kotlin documentation on Basic Syntax and
Classes to write my Reminder
class. Kotlin provides lots of syntactic sugar compared to Java. I'll need time to get used to all of it. In this example, I define a class with a public property, and a constructor that takes one value and assigns that value to the property in a single line of code. So concise!
package com.wafrat.rappel.data
class Reminder(val label: String)
After running the app on my phone, I check the Firebase console and... here it is. My first reminder! Hurray!
Types and subtypes
Then I went ahead and defined the other classes. I made a Trigger
class, and 2 subtypes: LocationTrigger
, DayOfWeekTrigger
. By default, classes cannot be extended, they are final. You have to declare them open
. I like how it makes code safer.
Reading data
To read a list of reminders, you add a listener to the reference to the reminders. It will be read when the listener is attached, and whenever the data changes. Then in the listener, you map the children to what you need.
val listener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val reminders: List<String> = dataSnapshot.children.mapNotNull { it.getValue<Reminder>(Reminder::class.java)!!.label }
Log.i(TAG, reminders.joinToString())
}
override fun onCancelled(databaseError: DatabaseError) {
Log.w(TAG, "listener:onCancelled", databaseError.toException())
}
}
val remindersReference = mDatabase.child("users").child(userId).child("reminders")
remindersReference.addValueEventListener(listener)
When deserializing, I got the following error: com.google.firebase.database.DatabaseException: Class com.wafrat.rappel.data.Reminder does not define a no-argument constructor.
I had to add a secondary constructor, as the Kotlin doc explains. So it went from:
class Reminder(val label: String, val triggers: List<Trigger>)
to
class Reminder(val label: String, val triggers: List<Trigger>) {
constructor() : this("", listOf())
}
After fixing this, it correctly listed my reminders:
09-20 20:46:53.796 13054-13054/com.wafrat.rappel I/ScrollingActivity: buy groceries, buy groceries
Authentication/Restricting access
The article on Firebase database security shows how to restrict access to data. In our case we want to let only the user access their own data. To test that it works, Firebase provides a very neat simulator, which has already saved me at least half an hour. In the following example, I attempt to read a user's reminders, and I am authenticated but not as that user. It fails.
In this example, I am authenticated as the user and reading their data. It works.
Now that the app can read from and write to the database, we can work on adding new reminders.