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

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

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

    View full-size slide

  2. @CodingDoug
    @CodingDoug
    Cloud hosted, realtime, NoSQL

    View full-size slide

  3. @CodingDoug
    @CodingDoug

    View full-size slide

  4. @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?

    View full-size slide

  5. @CodingDoug


    Your
    Arch
    WW D?

    View full-size slide

  6. @CodingDoug
    Data Binding
    Lifecycles
    LiveData
    Navigation
    Paging
    Room
    ViewModel
    WorkManager

    View full-size slide

  7. @CodingDoug
    LiveData
    ViewModel
    Notify views when underlying database
    changes
    Manage UI-related data in a lifecycle-
    conscious way

    View full-size slide

  8. @CodingDoug
    @CodingDoug

    View full-size slide

  9. @CodingDoug
    @CodingDoug

    View full-size slide

  10. @CodingDoug
    @CodingDoug

    View full-size slide

  11. @CodingDoug
    @CodingDoug

    View full-size slide

  12. @CodingDoug
    Handle stock price changes from a document in Firestore

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  15. @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) {
    }
    }

    View full-size slide

  16. @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) {
    }
    }

    View full-size slide

  17. @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.")
    }
    }

    View full-size slide

  18. @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.")
    }
    }

    View full-size slide

  19. @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.")
    }
    }

    View full-size slide

  20. @CodingDoug

    LiveData

    View full-size slide

  21. @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.

    View full-size slide

  22. @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(R.id.tv_ticker)
    val priceTextView = findViewById(R.id.tv_price)
    }
    }

    View full-size slide

  23. @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(R.id.tv_ticker)
    val priceTextView = findViewById(R.id.tv_price)
    val liveData: LiveData = getLiveDataForMyTicker("HSTK")
    }
    }

    View full-size slide

  24. @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(R.id.tv_ticker)
    val priceTextView = findViewById(R.id.tv_price)
    val liveData: LiveData = getLiveDataForMyTicker("HSTK")
    liveData.observe(this@MyActivity, Observer { stockPrice ->
    })
    }
    }

    View full-size slide

  25. @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(R.id.tv_ticker)
    val priceTextView = findViewById(R.id.tv_price)
    val liveData: LiveData = getLiveDataForMyTicker("HSTK")
    liveData.observe(this@MyActivity, Observer { stockPrice ->
    if (stockPrice != null) {
    tickerTextView.text = stockPrice.ticker
    priceTextView.text = stockPrice.price.toString()
    }
    })
    }
    }

    View full-size slide

  26. @CodingDoug
    LiveData is aware of the Android Activity lifecycle

    View full-size slide

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

    View full-size slide

  28. @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
    }

    View full-size slide

  29. @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!
    }

    View full-size slide

  30. @CodingDoug
    Implement LiveData with Firestore document updates
    class StockPriceLiveData()
    {
    }

    View full-size slide

  31. @CodingDoug
    Implement LiveData with Firestore document updates
    class StockPriceLiveData()
    : LiveData() {
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  35. @CodingDoug
    Implement LiveData with Firestore document updates
    class StockPriceLiveData(private val documentReference: DocumentReference)
    : LiveData() {
    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...
    }
    }

    View full-size slide

  36. @CodingDoug
    Implement LiveData with Firestore document updates
    class StockPriceLiveData(private val documentReference: DocumentReference)
    : LiveData() {
    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()
    }
    }

    View full-size slide

  37. @CodingDoug
    Implement LiveData with Firestore document updates
    class StockPriceLiveData(private val documentReference: DocumentReference)
    : LiveData(), EventListener {
    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?) {}
    }

    View full-size slide

  38. @CodingDoug
    Implement LiveData with Firestore document updates
    override fun onEvent(snap: DocumentSnapshot?, e: FirebaseFirestoreException?) {
    }

    View full-size slide

  39. @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()
    )
    }
    }

    View full-size slide

  40. @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)
    }
    }

    View full-size slide

  41. @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.")
    }
    }

    View full-size slide

  42. @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

    View full-size slide

  43. @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

    View full-size slide

  44. @CodingDoug
    ViewModel
    [ LiveData ]

    View full-size slide

  45. @CodingDoug
    Implement a ViewModel that yields a LiveData
    class StocksViewModel : ViewModel() {
    }

    View full-size slide

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

    View full-size slide

  47. @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 {
    }
    }

    View full-size slide

  48. @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
    }

    View full-size slide

  49. @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
    }

    View full-size slide

  50. @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()
    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
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  53. @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 {
    }
    }

    View full-size slide

  54. @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)
    }
    }

    View full-size slide

  55. @CodingDoug
    LiveData &

    ViewModel &

    Repository &

    Firebase

    View full-size slide

  56. @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...
    }
    }

    View full-size slide

  57. @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

    View full-size slide

  58. @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")
    }
    }

    View full-size slide

  59. @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 ->
    })
    }
    }

    View full-size slide

  60. @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 ->
    if (stockPrice != null) {
    tickerTextView.text = stockPrice.ticker
    priceTextView.text = stockPrice.price.toString()
    }
    })
    }
    }

    View full-size slide

  61. @CodingDoug
    What about errors?

    View full-size slide

  62. @CodingDoug
    ● LiveData doesn’t propagate errors
    ● Errors need to bubble up to the UI

    data class DataOrException(val data: T?, val exception: E?)
    typealias StockPriceOrException = DataOrException
    Now With this, now you can do:

    LiveData => LiveData
    What about errors?

    View full-size slide

  63. @CodingDoug
    But seriously: query results

    View full-size slide

  64. @CodingDoug
    @CodingDoug

    View full-size slide

  65. @CodingDoug
    Firestore query primer
    FirebaseFirestore.getInstance()
    .collection("stocks-live")

    View full-size slide

  66. @CodingDoug
    Firestore query primer
    FirebaseFirestore.getInstance()
    .collection("stocks-live")
    .whereGreaterThan("price", 100)

    View full-size slide

  67. @CodingDoug
    Firestore query primer
    FirebaseFirestore.getInstance()
    .collection("stocks-live")
    .whereGreaterThan("price", 100)
    .orderBy("price", Query.Direction.DESCENDING)

    View full-size slide

  68. @CodingDoug
    Firestore query primer
    FirebaseFirestore.getInstance()
    .collection("stocks-live")
    .whereGreaterThan("price", 100)
    .orderBy("price", Query.Direction.DESCENDING)
    .get()

    View full-size slide

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

    View full-size slide

  70. @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!

    View full-size slide

  71. @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 {
    override fun onEvent(snap: QuerySnapshot?, exception: FirebaseFirestoreException?) {
    }
    }

    View full-size slide

  72. @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 {
    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()
    }
    }
    }
    }

    View full-size slide

  73. @CodingDoug
    LiveData // deltas not gonna work, sorry!

    LiveData>
    One problem: LiveData doesn’t support deltas

    View full-size slide

  74. @CodingDoug
    LiveData // deltas not gonna work, sorry!

    LiveData>
    LiveData> // this is what we need: whole-resource updates
    One problem: LiveData doesn’t support deltas

    View full-size slide

  75. @CodingDoug
    LiveData // deltas not gonna work, sorry!

    LiveData>
    LiveData> // this is what we need: whole-resource updates
    LiveData, Exception>> // gotta handle errors
    One problem: LiveData doesn’t support deltas

    View full-size slide

  76. @CodingDoug
    LiveData // deltas not gonna work, sorry!

    LiveData>
    LiveData> // this is what we need: whole-resource updates
    LiveData, Exception>> // gotta handle errors
    // better this, for flexibility and interop with Jetpack and RecyclerView

    LiveData>, Exception>>
    interface QueryItem {

    val item: T

    val id: String

    }
    One problem: LiveData doesn’t support deltas

    View full-size slide

  77. @CodingDoug
    // Generic for all types of Firestore and Realtime Database queries

    typealias QueryResultsOrException = DataOrException>, E>
    Let’s escape generics hell! All hail Kotlin.

    View full-size slide

  78. @CodingDoug
    // Generic for all types of Firestore and Realtime Database queries

    typealias QueryResultsOrException = DataOrException>, E>
    // Specific for stock price query results - not so bad after all?

    typealias StockPriceQueryResults =

    QueryResultsOrException
    Let’s escape generics hell! All hail Kotlin.

    View full-size slide

  79. @CodingDoug
    // Generic for all types of Firestore and Realtime Database queries

    typealias QueryResultsOrException = DataOrException>, E>
    // Specific for stock price query results - not so bad after all?

    typealias StockPriceQueryResults =

    QueryResultsOrException
    // This sort of thing is what our views want to consume!

    // Our repository should deal out instances of this:

    LiveData
    Let’s escape generics hell! All hail Kotlin.

    View full-size slide

  80. @CodingDoug
    Implement FirestoreQueryLiveData
    typealias DocumentSnapshotsOrException =
    DataOrException?, FirebaseFirestoreException?>
    class FirestoreQueryLiveData(private val query: Query)
    : LiveData() {
    }

    View full-size slide

  81. @CodingDoug
    Implement FirestoreQueryLiveData
    typealias DocumentSnapshotsOrException =
    DataOrException?, FirebaseFirestoreException?>
    class FirestoreQueryLiveData(private val query: Query)
    : LiveData() {
    override fun onActive() {
    }
    override fun onInactive() {
    }
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  84. @CodingDoug
    Implement FirestoreQueryLiveData
    typealias DocumentSnapshotsOrException =
    DataOrException?, FirebaseFirestoreException?>
    class FirestoreQueryLiveData(private val query: Query)
    : LiveData(), EventListener {
    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 = snapshot?.documents
    postValue(DocumentSnapshotsOrException(documents, e))
    }
    }

    View full-size slide

  85. @CodingDoug
    1. Transform: LiveData

    To : LiveData

    (LiveData>, 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

    View full-size slide

  86. @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!

    View full-size slide

  87. @CodingDoug
    DiffUtil.ItemCallback (from support lib source)
    public abstract static class ItemCallback {
    }

    View full-size slide

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

    View full-size slide

  89. @CodingDoug
    DiffUtil.ItemCallback (from support lib source)
    public abstract static class ItemCallback {
    // 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);
    }

    View full-size slide

  90. @CodingDoug
    DiffUtil.ItemCallback (from support lib source)
    public abstract static class ItemCallback {
    // 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;
    }
    }

    View full-size slide

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

    View full-size slide

  92. @CodingDoug
    Implement DiffUtil.ItemCallback for QueryItem
    // If your query yields List objects…
    interface QueryItem {
    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 : DiffUtil.ItemCallback>() {
    override fun areItemsTheSame(oldItem: QueryItem, newItem: QueryItem): Boolean {
    return oldItem.id == newItem.id
    }
    override fun areContentsTheSame(oldItem: QueryItem, newItem: QueryItem): Boolean {
    return oldItem.item == newItem.item
    }
    }
    Magic Kotlin equals() checks all data class properties

    View full-size slide

  93. @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?

    View full-size slide

  94. @CodingDoug
    What about background sync

    for offline use?

    View full-size slide

  95. @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.

    View full-size slide

  96. @CodingDoug
    1. App periodically polls the data store
    2. Backend notifies app when data is ready
    Two choices for background sync

    View full-size slide

  97. @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

    View full-size slide

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

    View full-size slide

  99. @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(1, TimeUnit.DAYS)
    .setConstraints(constraints)
    .build()

    View full-size slide

  100. @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(1, TimeUnit.DAYS)
    .setConstraints(constraints)
    .build()
    WorkManager.getInstance()
    .enqueueUniquePeriodicWork(
    "syncStocks",
    ExistingPeriodicWorkPolicy.KEEP,
    request)

    View full-size slide

  101. @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)

    View full-size slide

  102. @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!

    View full-size slide

  103. @CodingDoug
    TKR1
    TKR2
    Messaging topics for each ticker - routed to subscribed devices
    TKR2
    TKR3
    TKR1
    TKR2
    TKR3
    topic
    topic
    topic

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  106. @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
    }
    }
    }

    View full-size slide

  107. @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
    }
    }

    View full-size slide

  108. @CodingDoug
    Schedule one-time sync work
    private fun syncTicker(ticker: String) {
    }

    View full-size slide

  109. @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()
    }

    View full-size slide

  110. @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()
    }

    View full-size slide

  111. @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()
    .setConstraints(constraints)
    .setInputData(data)
    .build()
    }

    View full-size slide

  112. @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()
    .setConstraints(constraints)
    .setInputData(data)
    .build()
    WorkManager.getInstance()
    .beginUniqueWork("sync_$ticker", ExistingWorkPolicy.REPLACE, workRequest)
    .enqueue()
    }

    View full-size slide

  113. @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
    }
    }

    View full-size slide

  114. @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

    View full-size slide

  115. Thank you!
    @CodingDoug
    firebase.google.com
    Get the code:
    Get the app:
    bit.ly/2NtAuQP
    bit.ly/2NtGedo

    View full-size slide