Firebase + Kotlin: Extensions, Coroutines and Flows

Firebase + Kotlin: Extensions, Coroutines and Flows

[Talk delivered at Droidcon EMEA 2020]

Android is now Kotlin-first, but most of the existing libraries and SDKs were written in Java.
Despite the effort that the Firebase team has put in coming up with KTX libraries for their Android SDK, there are still a few Kotlin features that have been missed out.

In this session, Rosário will talk about some of the features that are present in the KTX libraries and the features that you can implement yourself to get cleaner and more idiomatic Kotlin code.

You’ll learn how Sequences, Sealed Classes, Coroutines and Asynchronous Flow can provide you with a kotlin-first experience when working with Firebase’s Android Java SDK.

66988ef2ff1307f07c7c8d829a6dc4fa?s=128

Rosário Pereira Fernandes

October 09, 2020
Tweet

Transcript

  1. Firebase + Kotlin Rosário Pereira Fernandes Firebase GDE @_rpfernandes Extensions,

    Coroutines & Flows
  2. What is Firebase?

  3. None
  4. “Android is now Kotlin-first” Google I/O 2019

  5. Firebase KTX libraries

  6. Views and Opinions are my own. And do not reflect

    those of the Firebase Team.
  7. // When not using Firebase KTX val dynamicLink = FirebaseDynamicLinks.getInstance().createDynamicLink()

    .setLink(Uri.parse("https://www.example.com/")) .setDomainUriPrefix("https://example.page.link") .setAndroidParameters( DynamicLink.AndroidParameters.Builder("com.example.android") .setMinimumVersion(16) .build()) .setIosParameters( DynamicLink.IosParameters.Builder("com.example.ios") .setAppStoreId("123456789") .setMinimumVersion("1.0.1") .build()) .buildDynamicLink()
  8. // Using Firebase KTX val dynamicLink = Firebase.dynamicLinks.dynamicLink { link

    = Uri.parse("https://www.example.com/") domainUriPrefix = "https://example.page.link" androidParameters("com.example.android") { minimumVersion = 16 } iosParameters("com.example.ios") { appStoreId = "123456789" minimumVersion = "1.0.1" } }
  9. Kotlin Language Features (the ones present in Firebase KTX) •

    Kotlin Extensions • Object Declarations • Inline Functions • Reified Type Parameters • Type-safe Builders • Destructuring Declarations • Sequences
  10. Kotlin Extensions Provides the ability to extend a class with

    new functionality without having to inherit from the class. // Extension Property val String.lastChar: Char = this[length - 1] // Extension Function fun Rectangle.getArea() { return length * width; } // DEMO fun usageDemo() { print(“Hello”.lastChar) // prints o val rect = Rectangle(2, 3) print(rect.getArea()) // prints 6 }
  11. Object Declarations Declares a class using the Singleton pattern. //

    Example object Coin { private var coin = 0 fun getCoin(): Int = coin fun addCoin() { coin += 10 } } // Usage Coin.getCoin() // returns 0 Coin.addCoin() Coin.getCoin() // returns 10 // from firebase-common-ktx object Firebase
  12. Kotlin Extensions Provides the ability to extend a class with

    new functionality without having to inherit from that class. val Firebase.database = FirebaseDatabase.getInstance() val Firebase.firestore = FirebaseFirestore.getInstance() fun Firebase.firestore(app: FirebaseApp) = FirebaseFirestore.getInstance(app) // Usage: val firestore = Firebase.firestore val firestore2 = Firebase.firestore(app)
  13. Inline Functions Tells the compiler to NOT treat our function

    as a Higher-Order function. // from firebase-messaging-ktx inline fun remoteMessage( to: String, init: RemoteMessage.Builder.() -> Unit ): RemoteMessage { val builder = RemoteMessage.Builder(to) builder.init() return builder.build() } // Usage remoteMessage(“user1”) { messageId = “123” data = hashMapOf() }
  14. Reified Type Parameters Allows us to access a type passed

    as a parameter. // from firebase-firestore inline fun <reified T> DocumentSnapshot.toObject(): T? = toObject(T::class.java) // Example val dataSnapshot = // … // Usage without Firebase KTX: dataSnapshot.toObject(Person::class.java) // Usage with Firebase KTX: dataSnapshot.toObject<Person>()
  15. Type-safe Builders Allow creating Kotlin-based domain-specific languages (DSLs) suitable for

    building complex hierarchical data structures in a semi-declarative way // from firebase-dynamic-links-ktx fun FirebaseDynamicLinks.dynamicLink(init: DynamicLink.Builder.() -> Unit): DynamicLink { val builder = FirebaseDynamicLinks.getInstance().createDynami cLink() builder.init() return builder.buildDynamicLink() } // Usage Firebase.dynamicLinks.dynamicLink { androidParameters { minimumVersion = 21 } iosParameters(“bundleId”) { appStoreId = “some-id” } }
  16. Destructuring Declarations Destructures an object into multiple variables at once.

    // Usage of firebase-storage // Without Firebase KTX uploadTask.addOnProgressListener { snapshot -> val bytesTransferred = snapshot.bytesTransferred val totalByteCount = snapshot.totalByteCount val progress = (100.0 * bytesTransferred) / totalByteCount Log.i(TAG, "Upload is $progress% done") } // With Firebase KTX uploadTask.addOnProgressListener{ (bytes, total) -> val progress = (100.0 * bytes) / total Log.i(TAG, "Upload is $progress% done") }
  17. Destructuring Declarations Destructures an object into multiple variables at once.

    // from firebase-storage-ktx operator fun UploadTask.TaskSnapshot.component1() = bytesTransferred operator fun UploadTask.TaskSnapshot.component2() = totalByteCount operator fun UploadTask.TaskSnapshot.component3() = metadata operator fun UploadTask.TaskSnapshot.component4() = uploadSessionUri
  18. Sequences Similar to Iterable, but multi-step processing is executed lazily

    (instead of eagerly). // Create a Sequence from Realtime Database // values (From an Iterable) var snapshot: DataSnapshot val sequence = snapshot.children.asSequence() // Create a Sequence from Cloud Firestore // values (From an Iterator) var snapshot: QuerySnapshot val sequence = snapshot.iterator().asSequence()
  19. // Using an Iterable val words = "The quick brown

    fox jumps over the lazy dog".split(" ") val lengthsList = words.filter { println("filter: $it"); it.length > 3 } .map { println("length: ${it.length}"); it.length } .take(4) println("Lengths of first 4 words longer than 3 chars:") println(lengthsList) image from kotlinlang.org
  20. // Using a Sequence val wordsSequence = "The quick brown

    fox jumps over the lazy dog".split(" ").asSequence() val lengthsSequence = wordsSequence.filter { println("filter: $it"); it.length > 3 } .map { println("length: ${it.length}"); it.length } .take(4) println("Lengths of first 4 words longer than 3 chars") println(lengthsSequence.toList()) // terminal operation image from kotlinlang.org
  21. Kotlin Language Features (the 404s in Firebase KTX) • Kotlin

    Coroutines • Asynchronous Flows • Sealed Classes
  22. Coroutines The so called “light-weight threads”. async/await → suspend A

    way to escape callback hell
  23. // Using the Tasks API without Coroutines usersRef.document("john").get().addOnSuccessListener { querySnapshot

    -> val johnUser = querySnapshot.toObject(User::class.java) friendsRef.get().addOnSuccessListener { friendSnapshot -> val friends = friendSnapshot.toObjects(Friend::class.java) showProfileAndFriends(johnUser, friends) }.addOnFailureListener { e -> displayError(e) } }.addOnFailureListener { e -> displayError(e) }
  24. // Add the kotlinx-coroutines-play-services dependencies { // ... Other Dependencies

    … // Coroutines implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' // Provides Extension Functions to use Coroutines with the Tasks API implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.3.9' }
  25. // Using the Tasks API with Coroutines suspend fun loadUserData()

    { try { val querySnapshot = usersRef.document("john").get().await() val johnUser = querySnapshot.toObject(User::class.java) val friendSnapshot = friendsRef.get().await() val friends = friendSnapshot.toObjects(Friend::class.java) showProfileAndFriends(johnUser, friends) } catch (e: FirebaseFirestoreException) { displayError(e) } } // Usage fun main() { GlobalScope.launch { // Avoid using GlobalScope. This is just an example loadUserData() } }
  26. Asynchronous Flow Returns multiple asynchronously computed values. // Example (No

    pre-computing) fun simple(): List<Int> = listOf(1, 2, 3) fun main() { simple().forEach { value -> println(value) } }
  27. Asynchronous Flow Returns multiple asynchronously computed values. // Computing using

    Sequences fun simple(): Sequence<Int> = sequence { for (i in 1..3) { Thread.sleep(100) // pretend we are computing it yield(i) // yield next value } } fun main() { simple().forEach { value -> println(value) } } // Blocks the main thread
  28. Asynchronous Flow Returns multiple asynchronously computed values. // Computing using

    suspend functions suspend fun simple(): List<Int> { delay(1000) // pretend we are computing it return listOf(1, 2, 3) } fun main() = runBlocking<Unit> { simple().forEach { value -> println(value) } } // Notice that we’re returning a List<Int>
  29. Asynchronous Flow Returns multiple asynchronously computed values. // Computing using

    Asynchronous Flow fun simple(): Flow<Int> = flow { for (i in 1..3) { delay(100) // pretend we are computing it emit(i) // emit next value } } fun main() = runBlocking<Unit> { // Collect the flow simple().collect { value -> println(value) } }
  30. Asynchronous Flow How to use it with Firebase? // (hopefully)

    coming soon to firestore-ktx // From github: // firebase/firebase-android-sdk#1252 fun Query.toFlow() = callbackFlow { val listener = addSnapshotListener { value, error -> if (value != null) { runCatching { offer(value) } } else if (error != null) { close(error) } } awaitClose { listener.remove() } } // PS: callbackFlow is still Experimental
  31. Sealed Classes Used for representing restricted class hierarchies, when a

    value can have one of the types from a limited set, but cannot have any other type // Example sealed class Result { data class Success(val data: Int) : Result data class Error(val exception: Exception) : Result object InProgress : Result } // Usage fun handleResult(result: Result) { when (result) { is Result.Success -> { // do something with result.data } is Result.Error -> { // do something with result.exception } is Result.InProgress -> { // this one has no value passed to it } } }
  32. Sealed Classes Using it to simplify usage of the Realtime

    Database. databaseReference.addChildEventListener(object : ChildEventListener { override fun onChildAdded(dataSnapshot: DataSnapshot, previousChildName: String?) { Log.d(TAG, "onChildAdded:" + dataSnapshot.key) } override fun onChildChanged(dataSnapshot: DataSnapshot, previousChildName: String?) { } override fun onChildRemoved(dataSnapshot: DataSnapshot) { } override fun onChildMoved(dataSnapshot: DataSnapshot, previousChildName: String?) { } override fun onCancelled(databaseError: DatabaseError) { } })
  33. Sealed Classes Using it to simplify usage of the Realtime

    Database. sealed class Child { data class Added(val dataSnapshot: DataSnapshot,val previousChildName: String?) : Child data class Changed(val dataSnapshot: DataSnapshot,val previousChildName: String?) : Child data class Removed(val dataSnapshot: DataSnapshot) : Child data class Moved(val dataSnapshot: DataSnapshot,val previousChildName: String?) : Child data class Cancelled(val databaseError: DatabaseError) : Child }
  34. Sealed Classes Sealed Classes + Flow to simplify the usage

    of the Realtime Database. fun DatabaseReference.childrenFlow() = callbackFlow { val listener = addChildEventListener(object : ChildEventListener { override fun onChildAdded(ds: DataSnapshot, s: String?) { runCatching { offer(Child.Added(ds, s)) } } override fun onChildChanged(ds: DataSnapshot, s: String?) { runCatching { offer(Child.Changed(ds, s)) } } // ... onChildMoved, onChild Removed override fun onCancelled(databaseError: DatabaseError) { close(databaseError) } }) awaitClose { listener.remove() } }
  35. Sealed Classes Sealed Classes + Flow to simplify the usage

    of the Realtime Database. // Usage var databaseRef: DatabaseReference = ... databaseRef.childrenFlow().collect { child -> if (child is Child.Added) { Log.d(TAG, "onChildAdded:" + child.dataSnapshot.key) } }
  36. Bonus 1: Firebase Performance Monitor // from: https://medium.com/firebase-developers/tracking-per formance-in-kotlin-suspending-functions-c81c01f87c9 2

    inline fun <E> trace(name : String, block: (Trace) -> E): E { val trace = startTrace(name) return try { block(trace) } finally { trace.stop() } } // Usage trace(“http-trace”) { // run the http request }
  37. Bonus 2: Set Custom Keys to Crashlytics fun FirebaseCrashlytics.setCustomKeys(vararg pairs:

    Pair<String, Any>) { for ((key, value) in pairs) { when (value) { is Boolean -> setCustomKey(key, value) is Double -> setCustomKey(key, value) is Float -> setCustomKey(key, value) is Int -> setCustomKey(key, value) is Long -> setCustomKey(key, value) is String -> setCustomKey(key, value) else -> { throw IllegalArgumentException("Illegal value type for key \"$key\"") } } } }
  38. Bonus 2: Set Custom Keys to Crashlytics // Usage Firebase.crashlytics.setCustomKeys(

    “str_key” to “hello”, “bool_key” to true, “int_key” to 123 )
  39. Additional Resources • The Firebase KTX libraries are Open Source:

    https://github.com/firebase/firebase-android-sdk • Firebase KTX Docs: https://firebaseopensource.com/projects/firebase/firebase-android-sdk/ • Coroutines Play Services: https://github.com/Kotlin/kotlinx.coroutines/tree/master/integration/kotlinx- coroutines-play-services
  40. Thank You! Rosário Pereira Fernandes Firebase GDE @_rpfernandes