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

Firebase & Jetpack: fit like a glove (360|AnDev, Denver)

Firebase & Jetpack: fit like a glove (360|AnDev, Denver)

3acd4fb373289e71fd7ebfb287a75a3b?s=128

Doug Stevenson

July 19, 2018
Tweet

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 @CodingDoug

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

  4. @CodingDoug @CodingDoug

  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?
  6. @CodingDoug <TextView/> <Button/> Your Arch WW D?

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

  8. @CodingDoug LiveData ViewModel Notify views when underlying database changes Manage

    UI-related data in a lifecycle-conscious way
  9. @CodingDoug @CodingDoug

  10. @CodingDoug @CodingDoug

  11. @CodingDoug @CodingDoug

  12. @CodingDoug @CodingDoug

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

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

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

    val firestore = FirebaseFirestore.getInstance() val ref = firestore.collection("stocks-live").document("HSTK")
  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) { } }
  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) { } }
  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.") } }
  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.") } }
  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.") } }
  21. @CodingDoug <TextView/> LiveData

  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.
  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<TextView>(R.id.tv_ticker) val priceTextView = findViewById<TextView>(R.id.tv_price) } }
  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<TextView>(R.id.tv_ticker) val priceTextView = findViewById<TextView>(R.id.tv_price) val liveData: LiveData<StockPrice> = getLiveDataForMyTicker("HSTK") } }
  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<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 -> }) } }
  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<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() } }) } }
  27. @CodingDoug LiveData is aware of the Android Activity lifecycle

  28. @CodingDoug LiveData is aware of the Android Activity lifecycle override

    fun onStop() { super.onStop() // LivaData becomes inactive, doesn't notify observers }
  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 }
  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! }
  31. @CodingDoug Implement LiveData with Firestore document updates class StockPriceLiveData() {

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

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

    documentReference: DocumentReference) : LiveData<StockPrice>() { }
  34. @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! } }
  35. @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(....) } }
  36. @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... } }
  37. @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() } }
  38. @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?) {} }
  39. @CodingDoug Implement LiveData with Firestore document updates override fun onEvent(snap:

    DocumentSnapshot?, e: FirebaseFirestoreException?) { }
  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() ) } }
  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) } }
  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.") } }
  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
  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
  45. @CodingDoug ViewModel

  46. @CodingDoug Implement a ViewModel that yields a LiveData class StocksViewModel

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

    : ViewModel() { fun getStockPriceLiveData(ticker: String): StockPriceLiveData { } }
  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 { } }
  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 }
  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 { return repository.getStockPriceLiveData(ticker) } } // StockDataRepository knows where the data actually comes from interface StockDataRepository { fun getStockPriceLiveData(ticker: String): StockPriceLiveData }
  51. @CodingDoug Implement a repository backed by Firestore interface StockDataRepository {

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

    fun getStockPriceLiveData(ticker: String): StockPriceLiveData } class FirestoreStockDataRepository : StockDataRepository { override fun getStockPriceLiveData(ticker: String): StockPriceLiveData { } }
  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 { } }
  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) } }
  55. @CodingDoug LiveData & ViewModel & Repository & Firebase

  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... } }
  57. @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) // Inflate and initialize views... val viewModel = ViewModelProviders.of(this).get(StocksViewModel::class.java) } }
  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) // Inflate and initialize views... val viewModel = ViewModelProviders.of(this).get(StocksViewModel::class.java) val liveData = viewModel.getStockPriceLiveData("HSTK") } }
  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) // Inflate and initialize views... val viewModel = ViewModelProviders.of(this).get(StocksViewModel::class.java) val liveData = viewModel.getStockPriceLiveData("HSTK") liveData.observe(this, Observer<StockPrice> { stockPrice -> }) } }
  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) // 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() } }) } }
  61. @CodingDoug

  62. @CodingDoug • LiveData<StockPrice> doesn’t propagate errors • Errors need to

    bubble up to the UI 
 class DataOrException<T, E: Exception?>(val data: T?, val exception: E?) typealias StockPriceOrException = DataOrException<StockPrice, Exception> LiveData<StockPrice> => LiveData<StockPriceOrException> What about errors?
  63. @CodingDoug • https://github.com/CodingDoug/firebase-jetpack • Sync individual documents/nodes, and queries ◦

    Query → RecyclerView support is inefficient and still needs work • Implementations for both Realtime Database and Firestore • Background sync with WorkManager • Backend implemented with TypeScript for node and Cloud Functions • Stay tuned for more! Lots more in the repo
  64. Thank you! @CodingDoug firebase.google.com Get the code: Get the app:

    bit.ly/2NtAuQP bit.ly/2NtGedo