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.
= 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?
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.") } }
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.") } }
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.") } }
updates. Genericized Subclasses must specify a type of object containing updates. Lifecycle-aware Handles component start, stop, and destroy states automatically.
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) } }
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.") } }
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
in reconfigured activities Shared & Managed System manages the scope of instances, may be shared Lifecycle-aware Automatically cleaned up on final destroy
: 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 }
: 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 }
: ViewModel() { // Find a repository object using dependency injection private val repository: StockDataRepository = ... private val cache = HashMap<String, StockPriceLiveData>() fun getStockPriceLiveData(ticker: String): StockPriceLiveData { val liveData = repository.getStockPriceLiveData(ticker) // Cache liveData in the HashMap - too much boring code to show return liveData } } // StockDataRepository knows where the data actually comes from interface StockDataRepository { fun getStockPriceLiveData(ticker: String): StockPriceLiveData }
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 { } }
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) } }
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
bubble up to the UI data class DataOrException<T, E: Exception?>(val data: T?, val exception: E?) typealias StockPriceOrException = DataOrException<StockPrice, Exception> Now With this, now you can do: LiveData<StockPrice> => LiveData<StockPriceOrException> What about errors?
// this is what we need: whole-resource updates LiveData<DataOrException<List<StockPrice>, Exception>> // gotta handle errors One problem: LiveData doesn’t support deltas
// this is what we need: whole-resource updates LiveData<DataOrException<List<StockPrice>, Exception>> // gotta handle errors // better this, for flexibility and interop with Jetpack and RecyclerView LiveData<DataOrException<List<QueryItem<StockPrice>>, Exception>> interface QueryItem<T> { val item: T val id: String } One problem: LiveData doesn’t support deltas
Database queries typealias QueryResultsOrException<T, E> = DataOrException<List<QueryItem<T>>, E> // Specific for stock price query results - not so bad after all? typealias StockPriceQueryResults = QueryResultsOrException<StockPrice, Exception> Let’s escape generics hell! All hail Kotlin.
Database queries typealias QueryResultsOrException<T, E> = DataOrException<List<QueryItem<T>>, E> // Specific for stock price query results - not so bad after all? typealias StockPriceQueryResults = QueryResultsOrException<StockPrice, Exception> // This sort of thing is what our views want to consume! // Our repository should deal out instances of this: LiveData<StockPriceQueryResults> Let’s escape generics hell! All hail Kotlin.
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!
ItemCallback<T> { // Return true if oldItem and newItem are the same item in the list public abstract boolean areItemsTheSame(T oldItem, T newItem); // Return true if oldItem and newItem contain the same data public abstract boolean areContentsTheSame(T oldItem, T newItem); }
ItemCallback<T> { // Return true if oldItem and newItem are the same item in the list public abstract boolean areItemsTheSame(T oldItem, T newItem); // Return true if oldItem and newItem contain the same data public abstract boolean areContentsTheSame(T oldItem, T newItem); // Return which individual properties changed in the item (optional) public Object getChangePayload(T oldItem, T newItem) { return null; } }
List<QueryItem> objects… interface QueryItem<T> { val item: T // your actual data item (e.g. StockPrice) val id: String // the database record id } // ... use this ItemCallback for all QueryItem T types that are Kotlin case classes! open class QueryItemDiffCallback<T> : DiffUtil.ItemCallback<QueryItem<T>>() { override fun areItemsTheSame(oldItem: QueryItem<T>, newItem: QueryItem<T>): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: QueryItem<T>, newItem: QueryItem<T>): Boolean { return oldItem.item == newItem.item } } Magic Kotlin equals() checks all data class properties
• 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?
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.
of execution) Constraints Indicate necessary conditions to operate (network, battery) Work chaining For complex tasks involving serial and/or parallel work
change over time val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build() val request = PeriodicWorkRequestBuilder<SyncStockPrices>(1, TimeUnit.DAYS) .setConstraints(constraints) .build()
◦ 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)
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!
String) { // Same constraints as before val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build() }
String) { // Same constraints as before val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build() val data: Data = mapOf("ticker" to ticker).toWorkData() }
String) { // Same constraints as before val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build() val data: Data = mapOf("ticker" to ticker).toWorkData() val workRequest = OneTimeWorkRequestBuilder<SingleStockPriceSyncWorker>() .setConstraints(constraints) .setInputData(data) .build() }
String) { // Same constraints as before val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build() val data: Data = mapOf("ticker" to ticker).toWorkData() val workRequest = OneTimeWorkRequestBuilder<SingleStockPriceSyncWorker>() .setConstraints(constraints) .setInputData(data) .build() WorkManager.getInstance() .beginUniqueWork("sync_$ticker", ExistingWorkPolicy.REPLACE, workRequest) .enqueue() }
doWork(): Result { val ticker = inputData.getString("ticker", null) // Do your work here synchronously return Result.SUCCESS // or RETRY, or FAILURE } }
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