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 Slide

  2. @CodingDoug

    View Slide

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

    View Slide

  4. @CodingDoug
    @CodingDoug

    View Slide

  5. @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 Slide

  6. @CodingDoug


    Your
    Arch
    WW D?

    View Slide

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

    View Slide

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

    View Slide

  9. @CodingDoug
    @CodingDoug

    View Slide

  10. @CodingDoug
    @CodingDoug

    View Slide

  11. @CodingDoug
    @CodingDoug

    View Slide

  12. @CodingDoug
    @CodingDoug

    View Slide

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

    View Slide

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

    View Slide

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

    View 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) {
    }
    else if (exception != null) {
    }
    }

    View 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) {
    }
    }

    View 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()
    )
    }
    else if (exception != null) {
    TODO("This is just a simple code sample, ain't got time for this.")
    }
    }

    View 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
    }
    else if (exception != null) {
    TODO("This is just a simple code sample, ain't got time for this.")
    }
    }

    View Slide

  20. @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 Slide

  21. @CodingDoug

    LiveData

    View Slide

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

    View 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")
    }
    }

    View 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 ->
    })
    }
    }

    View Slide

  26. @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 Slide

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

    View Slide

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

    View 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
    }

    View Slide

  30. @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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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...
    }
    }

    View Slide

  37. @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 Slide

  38. @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 Slide

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

    View 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()
    )
    }
    }

    View 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)
    }
    }

    View Slide

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

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

  44. @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 Slide

  45. @CodingDoug
    ViewModel
    [ LiveData ]

    View Slide

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

    View Slide

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

    View 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 {
    }
    }

    View 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 {
    }
    }
    // StockDataRepository knows where the data actually comes from
    interface StockDataRepository {
    fun getStockPriceLiveData(ticker: String): StockPriceLiveData
    }

    View Slide

  50. @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 Slide

  51. @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 Slide

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

    View Slide

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

    View 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 {
    }
    }

    View Slide

  55. @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 Slide

  56. @CodingDoug
    LiveData &

    ViewModel &

    Repository &

    Firebase

    View Slide

  57. @CodingDoug

    View 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...
    }
    }

    View Slide

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

    View Slide

  61. @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 Slide

  62. @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 Slide

  63. @CodingDoug
    What about errors?

    View Slide

  64. @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 Slide

  65. @CodingDoug
    But seriously: query results

    View Slide

  66. @CodingDoug
    @CodingDoug

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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

    View 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!

    View Slide

  73. @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 Slide

  74. @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 Slide

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

    LiveData>
    One problem: LiveData doesn’t support deltas

    View Slide

  76. @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 Slide

  77. @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 Slide

  78. @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 Slide

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

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

    View Slide

  80. @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 Slide

  81. @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 Slide

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

    View Slide

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

    View Slide

  84. @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 Slide

  85. @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 Slide

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

  87. @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 Slide

  88. @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 Slide

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

    View 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);
    }

    View Slide

  91. @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 Slide

  92. @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 Slide

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

  94. @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 Slide

  95. @CodingDoug

    View Slide

  96. @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 Slide

  97. @CodingDoug
    What about background sync

    for offline use?

    View Slide

  98. @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 Slide

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

    View Slide

  100. @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 Slide

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

    View Slide

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

  103. @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 Slide

  104. @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 Slide

  105. @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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

  109. @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 Slide

  110. @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 Slide

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

    View 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()
    }

    View Slide

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

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

  115. @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 Slide

  116. @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 Slide

  117. @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 Slide

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

    View Slide