From Firebase to UI with RxJava, MVP and Kotlin
David Vávra
@vavradav, +David Vávra, medium.com/@david.vavra
#mobcon
About me
Founder&CEO at
Android and backend hacker of Settle Up
Google Developer Expert for
Organizer at GDG Prague
Current Settle Up
Redesigned Settle Up
Firebase Realtime Database
Serverless
Sync
Offline
Realtime
Database structure
{
"groups": {
"group_id_1": {
"convertedToCurrency": "USD",
"inviteLink": "https://a3bc5.app.goo.gl/4564ggfdgfd",
"inviteLinkActive": true,
"minimizeDebts": true,
"name": "Eurotrip"
}
},
"permissions": {
"group_id_1": {
"user_id_1": {
"level": 30
},
"user_id_2": {
"level": 20
},
"user_id_3": {
"level": 10
}
}
},
"users": {
"user_id_1": {
"authProvider": "google",
"email": "me@destil.cz",
"name": "David Vávra",
"photoUrl": "https://lh3.googleusercontent.com/.../photo.jpg"
},
...
},
"userGroups": {
"user_id_1": {
"group_id_1": {
"color": "#795548"
}
},
...
}
}
Database Rules
{
"rules": {
"groups": {
"$groupId": {
".read": "root.child('permissions/'+$groupId+'/'+auth.uid+'/level').val() >= 10",
".write": "root.child('permissions/'+$groupId+'/'+auth.uid+'/level').val() >= 30"
}
},
"permissions": {
"$groupId": {
".read": "root.child('permissions/'+$groupId+'/'+auth.uid+'/level').val() >= 10",
".write":"(auth !== null && !data.exists() && !root.child('groups/'+$groupId).exists()) || root.child('permissions/'+$groupId+'/'+auth.uid+'/level').val() >= 30",
"$userId":{
".write":"(!newData.exists() && auth.uid === $userId) || (!data.exists() && newData.exists() && newData.child('level').val() <= 20)"
}
}
},
"users": {
"$userId": {
".read": "auth !== null",
".write": "auth.uid === $userId"
}
}
}
RxJava
Reactive
Functional
Background
Steep learning
Firebase to RxJava 1.2.7
@Nullable
public static Observable<DataSnapshot> observe(final Query query) {
return Observable.create(new Action1<Emitter<DataSnapshot>>() {
@Override
public void call(final Emitter<DataSnapshot> dataSnapshotEmitter) {
final ValueEventListener valueEventListener = query.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
dataSnapshotEmitter.onNext(dataSnapshot);
}
@Override
public void onCancelled(DatabaseError databaseError) {
dataSnapshotEmitter.onNext(null);
}
});
dataSnapshotEmitter.setCancellation(new Cancellable() {
@Override
public void cancel() throws Exception {
query.removeEventListener(valueEventListener);
}
});
}
}, Emitter.BackpressureMode.BUFFER)
.observeOn(Schedulers.computation());
}
Firebase to RxKotlin 1.2.7
fun Query.observe(): Observable<DataSnapshot?> {
return Observable.create<DataSnapshot?>({
val listener = this.addValueEventListener(object : ValueEventListener {
override fun onCancelled(databaseError: DatabaseError) {
it.onNext(null)
}
override fun onDataChange(dataSnapshot: DataSnapshot) {
it.onNext(dataSnapshot)
}
})
it.setCancellation {
this.removeEventListener(listener)
}
}, Emitter.BackpressureMode.BUFFER)
.observeOn(Schedulers.computation())
}
Kotlin
Concise
Interoperable
Functional
Easy to learn
Reading database
object DatabaseRead {
fun group(groupId: String): Observable<Group?> {
return DatabaseQuery().apply { path = "groups/$groupId" }
.observe()
.toObjectObservable(Group::class.java)
}
fun user(userId: String?): Observable<User?> {
return DatabaseQuery().apply { path = "users/$userId" }
.observe()
.toObjectObservable(User::class.java)
}
fun permissions(groupId: String): Observable<List<Permission>?> {
return DatabaseQuery().apply { path = "permissions/$groupId" }
.observe()
.toListObservable(Permission::class.java)
}
fun groupLinkEnabled(groupId: String): Observable<Boolean?> {
return DatabaseQuery().apply { path = "groups/$groupId/inviteLinkActive" }
.observe()
.toPrimitiveObservable(Boolean::class.java)
}
fun groupColor(groupId: String): Observable<String?> {
return DatabaseQuery()
.apply { path = "userGroups/${Auth.getUserId()}/$groupId/color" }
.observe()
.toPrimitiveObservable(String::class.java)
}
}
Parsing DataSnapshots
fun <T> Observable<DataSnapshot?>
.toPrimitiveObservable(type: Class<T>): Observable<T?> {
return this.map {
if (it == null) {
return@map null
}
it.getValue(type)
}
}
fun <T : DatabaseModel> Observable<DataSnapshot?>
.toObjectObservable(type: Class<T>): Observable<T?> {
return this.map {
if (it == null) {
return@map null
}
val data = it.getValue(type)
data?.setId(it.key)
data
}
}
fun <T : DatabaseModel> Observable<DataSnapshot?>
.toListObservable(type: Class<T>): Observable<List<T>?> {
return this.map {
if (it == null) {
return@map null
}
it.children.map {
val data = it.getValue(type)
data!!.setId(it.key)
data
}
}
}
Combining multiple database queries
fun permissionsUsers(groupId: String): Observable<PermissionsUsers?> {
return DatabaseRead.permissions(groupId)
.flatMap {
Observable.from(it).map { combineLatest(DatabaseRead.user(it.getId()), Observable.just(it.level), ::UserLevel) }.toList()
}
.flatMap {
Observable.combineLatest(it) {
var owner: User? = null
val editPermissions = mutableListOf<User>()
val readOnlyPermissions = mutableListOf<User>()
it.forEach {
it as UserLevel
when (it.level) {
Permission.LEVEL_OWNER -> owner = it.user
Permission.LEVEL_WRITE -> editPermissions.add(it.user)
Permission.LEVEL_READONLY -> readOnlyPermissions.add(it.user)
}
}
PermissionsUsers(owner!!, editPermissions, readOnlyPermissions) // Owner must exist in each group
}
}
}
data class UserLevel(val user: User, val level: Int)
data class PermissionsUsers(val owner: User, val editPermissions: List<User>, val readOnlyPermissions: List<User>)
Combining multiple database queries
fun permissionsUsers(groupId: String): Observable<PermissionsUsers?> {
return DatabaseRead.permissions(groupId)
.flatMap {
Observable.from(it).map { combineLatest(DatabaseRead.user(it.getId()), Observable.just(it.level), ::UserLevel) }.toList()
}
.flatMap {
Observable.combineLatest(it) {
var owner: User? = null
val editPermissions = mutableListOf<User>()
val readOnlyPermissions = mutableListOf<User>()
it.forEach {
it as UserLevel
when (it.level) {
Permission.LEVEL_OWNER -> owner = it.user
Permission.LEVEL_WRITE -> editPermissions.add(it.user)
Permission.LEVEL_READONLY -> readOnlyPermissions.add(it.user)
}
}
PermissionsUsers(owner!!, editPermissions, readOnlyPermissions) // Owner must exist in each group
}
}
}
data class UserLevel(val user: User, val level: Int)
data class PermissionsUsers(val owner: User, val editPermissions: List<User>, val readOnlyPermissions: List<User>)
Observable<List<Permission>?>
List<Permission>?
Observable<Permission>
Permission
Observable<List<Observable<UserLevel>>>
List<Observable<UserLevel>>
MVP
Single responsibility
Testable
Simple activities
Maintainable
Presenter & MvpView
class PermissionsPresenter(groupId: String) : BasePresenter<PermissionsMvpView>() {
override fun onCreatedByLoader() {
load(DatabaseCombine.groupColor(groupId), {
getView().applyGroupColor(it)
})
load(DatabaseRead.groupLinkEnabled(groupId)) {
getView().setInviteLinkEnabled(it)
}
load(DatabaseCombine.permissionsUsers(groupId)) {
getView().setGroupOwner(mOwner)
getView().setEditPermissions(it.editPermissions, isOwner())
getView().setReadOnlyPermissions(it.readOnlyPermissions, isOwner())
}
}
}
interface PermissionsMvpView : MvpView {
fun applyGroupColor(color: Int)
fun setGroupOwner(groupOwner: User)
fun setEditPermissions(users: List<User>,
isOwner: Boolean)
fun setReadOnlyPermissions(users: List<User>,
isOwner: Boolean)
fun setInviteLinkEnabled(enabled: Boolean)
}
BasePresenter
abstract class BasePresenter<V : MvpView> : Presenter<V> {
protected var mvpView: V? = null
private var mData = mutableListOf<PresenterData<*>>()
override fun onDestroyedByLoader() {
mData.forEach {
it.subscription?.unsubscribe()
}
}
@CallSuper
override fun onViewAttached(view: V) {
this.mvpView = view
mData.forEach {
val data = it as PresenterData<Any>
if (data.cachedValue != null) {
data.displayToView(data.cachedValue!!)
}
}
}
fun getView(): V {
return mvpView!!
}
�
fun <T> load(observable: Observable<T?>, displayToView: (T) -> Unit) {
val data = PresenterData(observable, displayToView)
data.subscription = data.observable
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
data.cachedValue = it
if (isViewAttached && it != null) {
data.displayToView(it)
}
}, {
logError(it)
})
mData.add(data)
}
}
Activity
class PermissionsActivity : GroupActivity<PermissionsPresenter, PermissionsMvpView>(), PermissionsMvpView {
override fun createPresenter(): PermissionsPresenter {
return PermissionsPresenter(getGroupId())
}
override fun applyGroupColor(color: Int) {
super.applyGroupColor(color)
vSwitch.setColor(color)
vFloatingActionMenu.applyGroupColor(color)
}
override fun setGroupOwner(groupOwner: User) {
vAvatar.loadAvatar(groupOwner)
vName.text = groupOwner.name
vEmail.text = groupOwner.email
}
override fun setEditPermissions(users: List<User>, isOwner: Boolean) {
mEditAccessAdapter.setData(users, PermissionsBinder(isOwner, R.menu.permission_edit_access, { user, permissionChange -> ... }))
}
override fun setReadOnlyPermissions(users: List<User>, isOwner: Boolean) {
mReadOnlyAccessAdapter.setData(users, PermissionsBinder(isOwner, R.menu.permission_read_only_access, { user, permissionChange -> ... }))
}
override fun setInviteLinkEnabled(enabled: Boolean) {
vSwitch.isChecked = enabled
}
...
}
TL;DR
Firebase is great and not just the database
RxJava is great for combining multiple async calls to Firebase
Operators map, flatMap and combineLatest are most useful
Build new apps in Kotlin, it has many advantages over Java and it’s easy to learn
MVP is a solid architecture, Presenters should survive orientation
All today’s code here: https://github.com/davidvavra/mobcon-sample-code
Bonus: Firebase for .NET
Q&A
#mobcon
Follow me:
twitter.com/vavradav
medium.com/@david.vavra