Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Firebase & Jetpack: fit like a glove (DroidCon ...

Firebase & Jetpack: fit like a glove (DroidCon NYC)

Join this session to get a deep dive into the use of Jetpack’s Android Architecture Components along with Firebase to keep your app’s data fully synchronized and automatically refreshed with live updates from Realtime Database and Firestore.

Doug Stevenson

August 27, 2018
Tweet

More Decks by Doug Stevenson

Other Decks in Technology

Transcript

  1. Firebase & Jetpack: fit like a glove Doug Stevenson @CodingDoug

    Get the code: Get the app: bit.ly/2NtAuQP bit.ly/2NtGedo
  2. @CodingDoug A simple example … or is it? val firestore

    = FirebaseFirestore.getInstance() val ref = firestore.collection("coll").document("id") ref.get() .addOnSuccessListener { snapshot -> // We have data! Now what? } .addOnFailureListener { exception -> // Oh, snap } How should I do async programming? Do I directly update my views here? What manages this singleton?
  3. @CodingDoug Handle stock price changes from a document in Firestore

    val firestore = FirebaseFirestore.getInstance()
  4. @CodingDoug Handle stock price changes from a document in Firestore

    val firestore = FirebaseFirestore.getInstance() val ref = firestore.collection("stocks-live").document("HSTK")
  5. @CodingDoug Handle stock price changes from a document in Firestore

    val firestore = FirebaseFirestore.getInstance() val ref = firestore.collection("stocks-live").document("HSTK") ref.addSnapshotListener { snapshot, exception -> if (snapshot != null) { } else if (exception != null) { } }
  6. @CodingDoug Handle stock price changes from a document in Firestore

    val firestore = FirebaseFirestore.getInstance() val ref = firestore.collection("stocks-live").document("HSTK") ref.addSnapshotListener { snapshot, exception -> if (snapshot != null) { val model = StockPrice( ticker = snapshot.id, price = snapshot.getDouble("price")!!.toFloat() ) } else if (exception != null) { } }
  7. @CodingDoug Handle stock price changes from a document in Firestore

    val firestore = FirebaseFirestore.getInstance() val ref = firestore.collection("stocks-live").document("HSTK") ref.addSnapshotListener { snapshot, exception -> if (snapshot != null) { val model = StockPrice( ticker = snapshot.id, price = snapshot.getDouble("price")!!.toFloat() ) } else if (exception != null) { TODO("This is just a simple code sample, ain't got time for this.") } }
  8. @CodingDoug Handle stock price changes from a document in Firestore

    val firestore = FirebaseFirestore.getInstance() val ref = firestore.collection("stocks-live").document("HSTK") ref.addSnapshotListener { snapshot, exception -> if (snapshot != null) { val model = StockPrice( ticker = snapshot.id, price = snapshot.getDouble("price")!!.toFloat() ) someTextView.text = model.ticker } else if (exception != null) { TODO("This is just a simple code sample, ain't got time for this.") } }
  9. @CodingDoug Handle stock price changes from a document in Firestore

    val firestore = FirebaseFirestore.getInstance() val ref = firestore.collection("stocks-live").document("HSTK") ref.addSnapshotListener { snapshot, exception -> if (snapshot != null) { val model = StockPrice( ticker = snapshot.id, price = snapshot.getDouble("price")!!.toFloat() ) someTextView.text = model.ticker throw PoorArchitectureException("let's rethink mixing data store and views”) } else if (exception != null) { TODO("This is just a simple code sample, ain't got time for this.") } }
  10. @CodingDoug @CodingDoug LiveData Observable Code can register observers to receive

    updates. Genericized Subclasses must specify a type of object containing updates. Lifecycle-aware Handles component start, stop, and destroy states automatically.
  11. @CodingDoug Keep views up to date with LiveData class MyActivity

    : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.stuff) val tickerTextView = findViewById<TextView>(R.id.tv_ticker) val priceTextView = findViewById<TextView>(R.id.tv_price) } }
  12. @CodingDoug Keep views up to date with LiveData class MyActivity

    : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.stuff) val tickerTextView = findViewById<TextView>(R.id.tv_ticker) val priceTextView = findViewById<TextView>(R.id.tv_price) val liveData: LiveData<StockPrice> = getLiveDataForMyTicker("HSTK") } }
  13. @CodingDoug Keep views up to date with LiveData class MyActivity

    : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.stuff) val tickerTextView = findViewById<TextView>(R.id.tv_ticker) val priceTextView = findViewById<TextView>(R.id.tv_price) val liveData: LiveData<StockPrice> = getLiveDataForMyTicker("HSTK") liveData.observe(this@MyActivity, Observer<StockPrice> { stockPrice -> }) } }
  14. @CodingDoug Keep views up to date with LiveData class MyActivity

    : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.stuff) val tickerTextView = findViewById<TextView>(R.id.tv_ticker) val priceTextView = findViewById<TextView>(R.id.tv_price) val liveData: LiveData<StockPrice> = getLiveDataForMyTicker("HSTK") liveData.observe(this@MyActivity, Observer<StockPrice> { stockPrice -> if (stockPrice != null) { tickerTextView.text = stockPrice.ticker priceTextView.text = stockPrice.price.toString() } }) } }
  15. @CodingDoug LiveData is aware of the Android Activity lifecycle override

    fun onStop() { super.onStop() // LivaData becomes inactive, doesn't notify observers }
  16. @CodingDoug LiveData is aware of the Android Activity lifecycle override

    fun onStop() { super.onStop() // LivaData becomes inactive, doesn't notify observers } override fun onStart() { super.onStart() // LivaData becomes active again, notifies observers with latest data }
  17. @CodingDoug LiveData is aware of the Android Activity lifecycle override

    fun onStop() { super.onStop() // LivaData becomes inactive, doesn't notify observers } override fun onStart() { super.onStart() // LivaData becomes active again, notifies observers with latest data } override fun onDestroy() { super.onDestroy() // LiveData removes all activity-scoped observers - no leaks! }
  18. @CodingDoug Implement LiveData with Firestore document updates class StockPriceLiveData(private val

    documentReference: DocumentReference) : LiveData<StockPrice>() { }
  19. @CodingDoug Implement LiveData with Firestore document updates class StockPriceLiveData(private val

    documentReference: DocumentReference) : LiveData<StockPrice>() { override fun onActive() { // # observers 0 -> 1 woo-hoo! someone cares! } }
  20. @CodingDoug Implement LiveData with Firestore document updates class StockPriceLiveData(private val

    documentReference: DocumentReference) : LiveData<StockPrice>() { private var listenerRegistration: ListenerRegistration? = null override fun onActive() { // # observers 0 -> 1 woo-hoo! someone cares! listenerRegistration = documentReference.addSnapshotListener(....) } }
  21. @CodingDoug Implement LiveData with Firestore document updates class StockPriceLiveData(private val

    documentReference: DocumentReference) : LiveData<StockPrice>() { private var listenerRegistration: ListenerRegistration? = null override fun onActive() { // # observers 0 -> 1 woo-hoo! someone cares! listenerRegistration = documentReference.addSnapshotListener(....) } override fun onInactive() { // # observers 1 -> 0 awww, everyone left... } }
  22. @CodingDoug Implement LiveData with Firestore document updates class StockPriceLiveData(private val

    documentReference: DocumentReference) : LiveData<StockPrice>() { private var listenerRegistration: ListenerRegistration? = null override fun onActive() { // # observers 0 -> 1 woo-hoo! someone cares! listenerRegistration = documentReference.addSnapshotListener(....) } override fun onInactive() { // # observers 1 -> 0 awww, everyone left... listenerRegistration!!.remove() } }
  23. @CodingDoug Implement LiveData with Firestore document updates class StockPriceLiveData(private val

    documentReference: DocumentReference) : LiveData<StockPrice>(), EventListener<DocumentSnapshot> { private var listenerRegistration: ListenerRegistration? = null override fun onActive() { // # observers 0 -> 1 woo-hoo! someone cares! listenerRegistration = documentReference.addSnapshotListener(this) } override fun onInactive() { // # observers 1 -> 0 awww, everyone left... listenerRegistration!!.remove() } override fun onEvent(snap: DocumentSnapshot?, e: FirebaseFirestoreException?) {} }
  24. @CodingDoug Implement LiveData with Firestore document updates override fun onEvent(snap:

    DocumentSnapshot?, e: FirebaseFirestoreException?) { if (snap != null && snap.exists()) { val model = StockPrice( snap.id, snap.getDouble("price")!!.toFloat() ) } }
  25. @CodingDoug Implement LiveData with Firestore document updates override fun onEvent(snap:

    DocumentSnapshot?, e: FirebaseFirestoreException?) { if (snap != null && snap.exists()) { val model = StockPrice( snap.id, snap.getDouble("price")!!.toFloat() ) // Here you go, all my admiring observers! Go update your UI! setValue(model) } }
  26. @CodingDoug Implement LiveData with Firestore document updates override fun onEvent(snap:

    DocumentSnapshot?, e: FirebaseFirestoreException?) { if (snap != null && snap.exists()) { val model = StockPrice( snap.id, snap.getDouble("price")!!.toFloat() ) // Here you go, all my admiring observers! Go update your UI! setValue(model) } else if (e != null) { TODO("You should handle errors. Do as I say, not as I do.") } }
  27. @CodingDoug • Who creates the instance of LiveData? ◦ StockPriceLiveData

    constructor exposes Firestore implementation details. ◦ Remember: views should know nothing of data store. • LiveData loses its data on configuration change. ◦ Would rather have immediate LiveData results after configuration change. Two problems to resolve
  28. @CodingDoug @CodingDoug ViewModel Survives configuration changes Same ViewModel instance appears

    in reconfigured activities Shared & Managed System manages the scope of instances, may be shared Lifecycle-aware Automatically cleaned up on final destroy
  29. @CodingDoug Implement a ViewModel that yields a LiveData class StocksViewModel

    : ViewModel() { fun getStockPriceLiveData(ticker: String): StockPriceLiveData { } }
  30. @CodingDoug Implement a ViewModel that yields a LiveData class StocksViewModel

    : ViewModel() { // Find a repository object using dependency injection private val repository: StockDataRepository = ... fun getStockPriceLiveData(ticker: String): StockPriceLiveData { } }
  31. @CodingDoug Implement a ViewModel that yields a LiveData class StocksViewModel

    : ViewModel() { // Find a repository object using dependency injection private val repository: StockDataRepository = ... fun getStockPriceLiveData(ticker: String): StockPriceLiveData { } } // StockDataRepository knows where the data actually comes from interface StockDataRepository { fun getStockPriceLiveData(ticker: String): StockPriceLiveData }
  32. @CodingDoug Implement a ViewModel that yields a LiveData class StocksViewModel

    : ViewModel() { // Find a repository object using dependency injection private val repository: StockDataRepository = ... fun getStockPriceLiveData(ticker: String): StockPriceLiveData { val liveData = repository.getStockPriceLiveData(ticker) return liveData } } // StockDataRepository knows where the data actually comes from interface StockDataRepository { fun getStockPriceLiveData(ticker: String): StockPriceLiveData }
  33. @CodingDoug Implement a ViewModel that yields a LiveData class StocksViewModel

    : ViewModel() { // Find a repository object using dependency injection private val repository: StockDataRepository = ... private val cache = HashMap<String, StockPriceLiveData>() fun getStockPriceLiveData(ticker: String): StockPriceLiveData { val liveData = repository.getStockPriceLiveData(ticker) // Cache liveData in the HashMap - too much boring code to show return liveData } } // StockDataRepository knows where the data actually comes from interface StockDataRepository { fun getStockPriceLiveData(ticker: String): StockPriceLiveData }
  34. @CodingDoug Implement a repository backed by Firestore interface StockDataRepository {

    fun getStockPriceLiveData(ticker: String): StockPriceLiveData }
  35. @CodingDoug Implement a repository backed by Firestore interface StockDataRepository {

    fun getStockPriceLiveData(ticker: String): StockPriceLiveData } class FirestoreStockDataRepository : StockDataRepository { override fun getStockPriceLiveData(ticker: String): StockPriceLiveData { } }
  36. @CodingDoug Implement a repository backed by Firestore interface StockDataRepository {

    fun getStockPriceLiveData(ticker: String): StockPriceLiveData } class FirestoreStockDataRepository : StockDataRepository { // Should use DI for this too private val firestore = FirebaseFirestore.getInstance() override fun getStockPriceLiveData(ticker: String): StockPriceLiveData { } }
  37. @CodingDoug Implement a repository backed by Firestore interface StockDataRepository {

    fun getStockPriceLiveData(ticker: String): StockPriceLiveData } class FirestoreStockDataRepository : StockDataRepository { // Should use DI for this too private val firestore = FirebaseFirestore.getInstance() override fun getStockPriceLiveData(ticker: String): StockPriceLiveData { val ref = firestore.collection("stocks-live").document(ticker) return StockPriceLiveData(ref) } }
  38. @CodingDoug Use ViewModel and LiveData in an activity class MyActivity

    : AppCompatActivity() { private lateinit var tickerTextView: TextView private lateinit var priceTextView: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Redacted: inflate and initialize views... } }
  39. @CodingDoug class MyActivity : AppCompatActivity() { private lateinit var tickerTextView:

    TextView private lateinit var priceTextView: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Redacted: inflate and initialize views... val viewModel = ViewModelProviders.of(this).get(StocksViewModel::class.java) } } Use ViewModel and LiveData in an activity
  40. @CodingDoug Use ViewModel and LiveData in an activity class MyActivity

    : AppCompatActivity() { private lateinit var tickerTextView: TextView private lateinit var priceTextView: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Redacted: inflate and initialize views... val viewModel = ViewModelProviders.of(this).get(StocksViewModel::class.java) val liveData = viewModel.getStockPriceLiveData("HSTK") } }
  41. @CodingDoug Use ViewModel and LiveData in an activity class MyActivity

    : AppCompatActivity() { private lateinit var tickerTextView: TextView private lateinit var priceTextView: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Redacted: inflate and initialize views... val viewModel = ViewModelProviders.of(this).get(StocksViewModel::class.java) val liveData = viewModel.getStockPriceLiveData("HSTK") liveData.observe(this, Observer<StockPrice> { stockPrice -> }) } }
  42. @CodingDoug Use ViewModel and LiveData in an activity class MyActivity

    : AppCompatActivity() { private lateinit var tickerTextView: TextView private lateinit var priceTextView: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Redacted: inflate and initialize views... val viewModel = ViewModelProviders.of(this).get(StocksViewModel::class.java) val liveData = viewModel.getStockPriceLiveData("HSTK") liveData.observe(this, Observer<StockPrice> { stockPrice -> if (stockPrice != null) { tickerTextView.text = stockPrice.ticker priceTextView.text = stockPrice.price.toString() } }) } }
  43. @CodingDoug • LiveData<StockPrice> doesn’t propagate errors • Errors need to

    bubble up to the UI 
 data class DataOrException<T, E: Exception?>(val data: T?, val exception: E?) typealias StockPriceOrException = DataOrException<StockPrice, Exception> Now With this, now you can do:
 LiveData<StockPrice> => LiveData<StockPriceOrException> What about errors?
  44. @CodingDoug Firestore query primer FirebaseFirestore.getInstance() .collection("stocks-live") .whereGreaterThan("price", 100) .orderBy("price", Query.Direction.DESCENDING)

    // .get().addOnSuccessListener { snap: QuerySnapshot -> ... } // single result set .addSnapshotListener(eventListener) // ever-changing results!
  45. @CodingDoug Firestore query primer FirebaseFirestore.getInstance() .collection("stocks-live") .whereGreaterThan("price", 100) .orderBy("price", Query.Direction.DESCENDING)

    // .get().addOnSuccessListener { snap: QuerySnapshot -> ... } // single result set .addSnapshotListener(eventListener) // ever-changing results! val eventListener = object : EventListener<QuerySnapshot> { override fun onEvent(snap: QuerySnapshot?, exception: FirebaseFirestoreException?) { } }
  46. @CodingDoug Firestore query primer FirebaseFirestore.getInstance() .collection("stocks-live") .whereGreaterThan("price", 100) .orderBy("price", Query.Direction.DESCENDING)

    // .get().addOnSuccessListener { snap: QuerySnapshot -> ... } // single result set .addSnapshotListener(eventListener) // ever-changing results! val eventListener = object : EventListener<QuerySnapshot> { override fun onEvent(snap: QuerySnapshot?, exception: FirebaseFirestoreException?) { // look at the snapshot for changes since last event snap?.documentChanges?.forEach { change: DocumentChange -> when (change.type) { DocumentChange.Type.ADDED -> TODO() DocumentChange.Type.MODIFIED -> TODO() DocumentChange.Type.REMOVED -> TODO() } } } }
  47. @CodingDoug LiveData<DocumentChange> // deltas not gonna work, sorry!
 LiveData<List<DocumentChange>> LiveData<List<StockPrice>>

    // this is what we need: whole-resource updates One problem: LiveData doesn’t support deltas
  48. @CodingDoug LiveData<DocumentChange> // deltas not gonna work, sorry!
 LiveData<List<DocumentChange>> LiveData<List<StockPrice>>

    // this is what we need: whole-resource updates LiveData<DataOrException<List<StockPrice>, Exception>> // gotta handle errors One problem: LiveData doesn’t support deltas
  49. @CodingDoug LiveData<DocumentChange> // deltas not gonna work, sorry!
 LiveData<List<DocumentChange>> LiveData<List<StockPrice>>

    // this is what we need: whole-resource updates LiveData<DataOrException<List<StockPrice>, Exception>> // gotta handle errors // better this, for flexibility and interop with Jetpack and RecyclerView
 LiveData<DataOrException<List<QueryItem<StockPrice>>, Exception>> interface QueryItem<T> {
 val item: T
 val id: String
 } One problem: LiveData doesn’t support deltas
  50. @CodingDoug // Generic for all types of Firestore and Realtime

    Database queries
 typealias QueryResultsOrException<T, E> = DataOrException<List<QueryItem<T>>, E> Let’s escape generics hell! All hail Kotlin.
  51. @CodingDoug // Generic for all types of Firestore and Realtime

    Database queries
 typealias QueryResultsOrException<T, E> = DataOrException<List<QueryItem<T>>, E> // Specific for stock price query results - not so bad after all?
 typealias StockPriceQueryResults =
 QueryResultsOrException<StockPrice, Exception> Let’s escape generics hell! All hail Kotlin.
  52. @CodingDoug // Generic for all types of Firestore and Realtime

    Database queries
 typealias QueryResultsOrException<T, E> = DataOrException<List<QueryItem<T>>, E> // Specific for stock price query results - not so bad after all?
 typealias StockPriceQueryResults =
 QueryResultsOrException<StockPrice, Exception> // This sort of thing is what our views want to consume!
 // Our repository should deal out instances of this:
 LiveData<StockPriceQueryResults> Let’s escape generics hell! All hail Kotlin.
  53. @CodingDoug Implement FirestoreQueryLiveData typealias DocumentSnapshotsOrException = DataOrException<List<DocumentSnapshot>?, FirebaseFirestoreException?> class FirestoreQueryLiveData(private

    val query: Query) : LiveData<DocumentSnapshotsOrException>() { private var listenerRegistration: ListenerRegistration? = null override fun onActive() { listenerRegistration = query.addSnapshotListener(....) } override fun onInactive() { listenerRegistration?.remove() } }
  54. @CodingDoug Implement FirestoreQueryLiveData typealias DocumentSnapshotsOrException = DataOrException<List<DocumentSnapshot>?, FirebaseFirestoreException?> class FirestoreQueryLiveData(private

    val query: Query) : LiveData<DocumentSnapshotsOrException>(), EventListener<QuerySnapshot> { private var listenerRegistration: ListenerRegistration? = null override fun onActive() { listenerRegistration = query.addSnapshotListener(this) } override fun onInactive() { listenerRegistration?.remove() } override fun onEvent(snapshot: QuerySnapshot?, e: FirebaseFirestoreException?) { } }
  55. @CodingDoug Implement FirestoreQueryLiveData typealias DocumentSnapshotsOrException = DataOrException<List<DocumentSnapshot>?, FirebaseFirestoreException?> class FirestoreQueryLiveData(private

    val query: Query) : LiveData<DocumentSnapshotsOrException>(), EventListener<QuerySnapshot> { private var listenerRegistration: ListenerRegistration? = null override fun onActive() { listenerRegistration = query.addSnapshotListener(this) } override fun onInactive() { listenerRegistration?.remove() } override fun onEvent(snapshot: QuerySnapshot?, e: FirebaseFirestoreException?) { val documents: List<DocumentSnapshot> = snapshot?.documents postValue(DocumentSnapshotsOrException(documents, e)) } }
  56. @CodingDoug 1. Transform: LiveData<DocumentSnapshotsOrException>
 To : LiveData<StockPriceQueryResults>
 (LiveData<DataOrException<List<QueryItem<StockPrice>>, Exception>>) ◦

    Mechanical, uses MediatorLiveData
 (use the source, Luke) 2. Observe it, update RecyclerView adapter ◦ Adapter.notifyDataSetChanged() ◦ Yuck! It’s janky to refresh all the item views! Beyond FirestoreQueryLiveData
  57. @CodingDoug • android.support.v7.recyclerview.extensions.ListAdapter • It’s like magic! ◦ You call:

    submitList(yourListWithData) ◦ It figures out exactly what changed, calls granular Adapter notifications:
 notifyItemChanged(int)
 notifyItemInserted(int)
 notifyItemRemoved(int)
 etc. ◦ What’s the catch? Subclass one abstract base class… RecyclerView extension ListAdapter to the rescue!
  58. @CodingDoug DiffUtil.ItemCallback<T> (from support lib source) public abstract static class

    ItemCallback<T> { // Return true if oldItem and newItem are the same item in the list public abstract boolean areItemsTheSame(T oldItem, T newItem); }
  59. @CodingDoug DiffUtil.ItemCallback<T> (from support lib source) public abstract static class

    ItemCallback<T> { // Return true if oldItem and newItem are the same item in the list public abstract boolean areItemsTheSame(T oldItem, T newItem); // Return true if oldItem and newItem contain the same data public abstract boolean areContentsTheSame(T oldItem, T newItem); }
  60. @CodingDoug DiffUtil.ItemCallback<T> (from support lib source) public abstract static class

    ItemCallback<T> { // Return true if oldItem and newItem are the same item in the list public abstract boolean areItemsTheSame(T oldItem, T newItem); // Return true if oldItem and newItem contain the same data public abstract boolean areContentsTheSame(T oldItem, T newItem); // Return which individual properties changed in the item (optional) public Object getChangePayload(T oldItem, T newItem) { return null; } }
  61. @CodingDoug Implement DiffUtil.ItemCallback<T> for QueryItem // If your query yields

    List<QueryItem> objects... interface QueryItem<T> { val item: T // your actual data item (e.g. StockPrice) val id: String // the database record id }
  62. @CodingDoug Implement DiffUtil.ItemCallback<T> for QueryItem // If your query yields

    List<QueryItem> objects… interface QueryItem<T> { val item: T // your actual data item (e.g. StockPrice) val id: String // the database record id } // ... use this ItemCallback for all QueryItem T types that are Kotlin case classes! open class QueryItemDiffCallback<T> : DiffUtil.ItemCallback<QueryItem<T>>() { override fun areItemsTheSame(oldItem: QueryItem<T>, newItem: QueryItem<T>): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: QueryItem<T>, newItem: QueryItem<T>): Boolean { return oldItem.item == newItem.item } } Magic Kotlin equals() checks all data class properties
  63. @CodingDoug • Fun to watch, possibly bad to interact with

    • Frustrating if touchable items move around on screen! • Probably not so bad for append-only query results • Consider using a “refresh” indicator when new data is available Do you really need “live” query results?
  64. @CodingDoug • Want to browse stock prices after market close

    on the subway ride home. • Realtime Database and Firestore have offline caching built in. • How do you sync data when the app isn’t running? • Just need to schedule a data sync to device! What about background sync for offline use? Offline data is cool.
  65. @CodingDoug 1. App periodically polls the data store 2. Backend

    notifies app when data is ready Two choices for background sync
  66. @CodingDoug WorkManager Scheduled work Indicate time of execution (and period

    of execution) Constraints Indicate necessary conditions to operate (network, battery) Work chaining For complex tasks involving serial and/or parallel work
  67. @CodingDoug Schedule periodic work // These APIs are alpha, may

    change over time val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build()
  68. @CodingDoug Schedule periodic work // These APIs are alpha, may

    change over time val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build() val request = PeriodicWorkRequestBuilder<SyncStockPrices>(1, TimeUnit.DAYS) .setConstraints(constraints) .build()
  69. @CodingDoug Schedule periodic work // These APIs are alpha, may

    change over time val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build() val request = PeriodicWorkRequestBuilder<SyncStockPrices>(1, TimeUnit.DAYS) .setConstraints(constraints) .build() WorkManager.getInstance() .enqueueUniquePeriodicWork( "syncStocks", ExistingPeriodicWorkPolicy.KEEP, request)
  70. @CodingDoug • Periodic work doesn’t have exact schedules like cron

    ◦ Over-zealous sync period? ◦ Could replace periodic with delayed work, but must reschedule after each run • Client must know when markets close • Individual stocks still may have some after-hours trading Periodic work is not the best option for stock sync (IMO)
  71. @CodingDoug • Server knows when trading stops • App doesn’t

    schedule anything at all • Use Firebase Cloud Messaging to notify the app • On notification, app schedules a sync with WorkManager ASAP Server push is much better!
  72. @CodingDoug TKR1 TKR2 Messaging topics for each ticker - routed

    to subscribed devices TKR2 TKR3 TKR1 TKR2 TKR3 topic topic topic
  73. @CodingDoug Receive messages from FCM // Message payload sent to

    topic "HSTK" // { data: { ticker: ‘HSTK' } } class MyFirebaseMessagingService : FirebaseMessagingService() { }
  74. @CodingDoug Receive messages from FCM // Message payload sent to

    topic "HSTK" // { data: { ticker: ‘HSTK' } } class MyFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { } }
  75. @CodingDoug Receive messages from FCM // Message payload sent to

    topic "HSTK" // { data: { ticker: ‘HSTK' } } class MyFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { val ticker = message.data["ticker"] if (ticker == null || ticker.isEmpty()) { return } } }
  76. @CodingDoug Receive messages from FCM // Message payload sent to

    topic "HSTK" // { data: { ticker: 'HSTK' } } class MyFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { val ticker = message.data["ticker"] if (ticker == null || ticker.isEmpty()) { return } syncTicker(ticker) } private fun syncTicker(ticker: String) { // Kick off work here } }
  77. @CodingDoug Schedule one-time sync work with WorkManager private fun syncTicker(ticker:

    String) { // Same constraints as before val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build() }
  78. @CodingDoug Schedule one-time sync work with WorkManager private fun syncTicker(ticker:

    String) { // Same constraints as before val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build() val data: Data = mapOf("ticker" to ticker).toWorkData() }
  79. @CodingDoug Schedule one-time sync work with WorkManager private fun syncTicker(ticker:

    String) { // Same constraints as before val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build() val data: Data = mapOf("ticker" to ticker).toWorkData() val workRequest = OneTimeWorkRequestBuilder<SingleStockPriceSyncWorker>() .setConstraints(constraints) .setInputData(data) .build() }
  80. @CodingDoug Schedule one-time sync work with WorkManager private fun syncTicker(ticker:

    String) { // Same constraints as before val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build() val data: Data = mapOf("ticker" to ticker).toWorkData() val workRequest = OneTimeWorkRequestBuilder<SingleStockPriceSyncWorker>() .setConstraints(constraints) .setInputData(data) .build() WorkManager.getInstance() .beginUniqueWork("sync_$ticker", ExistingWorkPolicy.REPLACE, workRequest) .enqueue() }
  81. @CodingDoug Implement worker class SingleStockPriceSyncWorker : Worker() { override fun

    doWork(): Result { val ticker = inputData.getString("ticker", null) // Do your work here synchronously return Result.SUCCESS // or RETRY, or FAILURE } }
  82. @CodingDoug • https://github.com/CodingDoug/firebase-jetpack • Implementations for both Realtime Database and

    Firestore • Sync individual documents/nodes, and queries • Jetpack Paging (infinite scroll without infinite query) • Backend implemented with TypeScript for node and Cloud Functions • Goal: derive a library of patterns to add to FirebaseUI • Stay tuned for more! Lots more in the repo