1 of 26

From Firebase to UI with RxJava, MVP and Kotlin

David Vávra

@vavradav, +David Vávra, medium.com/@david.vavra

#mobcon

2 of 26

About me

Founder&CEO at

Android and backend hacker of Settle Up

Google Developer Expert for

Organizer at GDG Prague

3 of 26

Current Settle Up

4 of 26

Redesigned Settle Up

5 of 26

6 of 26

7 of 26

Firebase Realtime Database

Serverless

Sync

Offline

Realtime

8 of 26

9 of 26

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"

}

},

...

}

}

10 of 26

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"

}

}

}

11 of 26

RxJava

Reactive

Functional

Background

Steep learning

12 of 26

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());

}

13 of 26

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

}

14 of 26

Kotlin

Concise

Interoperable

Functional

Easy to learn

15 of 26

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)

}

}

16 of 26

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

}

}

}

17 of 26

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

18 of 26

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

19 of 26

MVP

Single responsibility

Testable

Simple activities

Maintainable

20 of 26

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)

}

21 of 26

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)

}

}

22 of 26

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

}

...

}

23 of 26

24 of 26

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

25 of 26

Bonus: Firebase for .NET

26 of 26

Q&A

#mobcon

Follow me:

twitter.com/vavradav

medium.com/@david.vavra