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. 1.

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

    Get the code: Get the app: bit.ly/2NtAuQP bit.ly/2NtGedo
  2. 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?
  3. 14.

    @CodingDoug Handle stock price changes from a document in Firestore

    val firestore = FirebaseFirestore.getInstance()
  4. 15.

    @CodingDoug Handle stock price changes from a document in Firestore

    val firestore = FirebaseFirestore.getInstance() val ref = firestore.collection("stocks-live").document("HSTK")
  5. 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) { } }
  6. 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) { } }
  7. 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.") } }
  8. 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.") } }
  9. 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.") } }
  10. 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.
  11. 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) } }
  12. 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") } }
  13. 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 -> }) } }
  14. 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() } }) } }
  15. 28.

    @CodingDoug LiveData is aware of the Android Activity lifecycle override

    fun onStop() { super.onStop() // LivaData becomes inactive, doesn't notify observers }
  16. 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 }
  17. 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! }
  18. 33.

    @CodingDoug Implement LiveData with Firestore document updates class StockPriceLiveData(private val

    documentReference: DocumentReference) : LiveData<StockPrice>() { }
  19. 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! } }
  20. 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(....) } }
  21. 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... } }
  22. 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() } }
  23. 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?) {} }
  24. 39.
  25. 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() ) } }
  26. 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) } }
  27. 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.") } }
  28. 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
  29. 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
  30. 47.

    @CodingDoug Implement a ViewModel that yields a LiveData class StocksViewModel

    : ViewModel() { fun getStockPriceLiveData(ticker: String): StockPriceLiveData { } }
  31. 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 { } }
  32. 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 }
  33. 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 }
  34. 51.

    @CodingDoug Implement a repository backed by Firestore interface StockDataRepository {

    fun getStockPriceLiveData(ticker: String): StockPriceLiveData }
  35. 52.

    @CodingDoug Implement a repository backed by Firestore interface StockDataRepository {

    fun getStockPriceLiveData(ticker: String): StockPriceLiveData } class FirestoreStockDataRepository : StockDataRepository { override fun getStockPriceLiveData(ticker: String): StockPriceLiveData { } }
  36. 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 { } }
  37. 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) } }
  38. 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... } }
  39. 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) } }
  40. 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") } }
  41. 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 -> }) } }
  42. 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() } }) } }
  43. 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?
  44. 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