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

badoo.problems.take(5).map { solve(it) }

Zsolt Kocsi
December 11, 2018

badoo.problems.take(5).map { solve(it) }

At Badoo, we have a long history when it comes to Android development. Since the beginning, the product has experienced huge growth, and our codebase and our team have grown with it. We now have over 400 million registrations, as well as several new apps to support. In this talk we’ll go on an architectural journey and discuss some of the major problems we needed to find solutions for in the last few years, before ending on what is ahead of us.

Zsolt Kocsi

December 11, 2018
Tweet

More Decks by Zsolt Kocsi

Other Decks in Programming

Transcript

  1. 1. MVP
 2. Clean architecture 3. ??? What we need

    Immutable state How to do it best ???
  2. 1. MVP
 2. Clean architecture 3. Proto-MVI Inspired by Redux

    Frankenstein’s monster of MVP Clean architecture Immutable state stores RxJava
  3. 1. MVP
 2. Clean architecture 3. Proto-MVI Pros Easier to

    reason about what can happen to the state State problems gone! Cons We needed structure
  4. R Fe class SimpleFeature : ReducerFeature<Wish, State, Nothing>( initialState =

    State(), reducer = ReducerImpl() ) { data class State( val counter: Int = 0 )£ sealed class Wish { object IncreaseCounter : Wish() data class MultiplyBy(val value: Int) : Wish() }• class ReducerImpl : Reducer<State, Wish> { override fun invoke(state: State, wish: Wish): State = when (wish) { IncreaseCounter -> state.copy( counter = state.counter + 1 )§ is MultiplyBy -> state.copy( counter = state.counter * wish.value )¡ }™ }¢ }
  5. data class State( val isLoading: Boolean = false, valªhasError:ªBoolean =

    false, val payload: Any? = null )• sealed£class£Wish£{ ¢¢¢¢object StartedLoading : Wish() ∞∞∞∞data class Success(val¶payload:¶Any)¶:¶Wish() §§§§data class Failed(valªthrowable:ªThrowable)ª: Wish() }º class ReducerImpl : Reducer<State, Wish> { override fun invoke(state: State, wish: Wish): State = when (wish) { StartedLoading -> state.copy( isLoading = true )º is Success -> state.copy( isLoading = false, hasError = false, payload = wish.payload )¡ is Failed -> state.copy( isLoading = false, hasError = true )™ R Fe
  6. class Reducer( private val initialState: State = State(), private val

    ex: Executor ) : StateReducer<State, Message, Effect> { override fun invoke(state: State, msg: Message): Pair<State, Command<Effect>?> = when (msg) { is Reset -> initialState to null is Get -> state to { ex.getSettings(state) }.takeIf { !state.loaded } is Retry -> state.retry() to { ex.getSettings(state)“}.takeIf { state.status == FAILURE } is Updated -> state.change(msg.setting) to { executor.saveSettings()≥}.takeIf { msg.saveImmediately }˜ is Done -> state to { ex.saveSettings(state.getValues(), state.mode) }∫ is Logout -> state to { ex.logout() }å is DeleteAccount -> state to { ex.deleteAccount() }~ is PublicProfile -> state.profile() to { ex.saveAndGet(state.profile()) ±}.takeIf { msg.mode == state.mode }^ is Get -> state.onGet(msg) to null is Save -> state.onSave(msg) to null is Logout -> state to null }@ }£ R Fe
  7. data class State( val isLoading: Boolean = false, valªhasError:ªBoolean =

    false, val payload: Any? = null )å≈ sealed•class•Wish•{ ••••object•LoadSomething•:•Wish() }Ω sealed£class£Effect£{ ¢¢¢¢object StartedLoading : Effect() ∞∞∞∞data class Success(val¶payload:¶Any)¶:¶Effect() §§§§data class Failed(valªthrowable:ªThrowable)ª:ªEffect() }º R Fe Ac
  8. data class State( val isLoading: Boolean = false, valªhasError:ªBoolean =

    false, val payload: Any? = null )å sealed£class£Effect£{ ¢¢¢¢object StartedLoading : Effect() ∞∞∞∞data class Success(val¶payload:¶Any)¶:¶Effect() §§§§data class Failed(valªthrowable:ªThrowable)ª:ªEffect() }™ class ReducerImpl : Reducer<State, Effect> { override fun invoke(state: State, effect: Effect): State = when (effect) { StartedLoading -> state.copy( isLoading = true ) is Success -> state.copy( isLoading = false, hasError = false, payload = wish.payload ) is Failed -> state.copy( isLoading = false, hasError = true ) R Fe Ac
  9. sealed class Wish { object LoadSomething : Wish() }¡ sealed

    class Effect { object StartedLoading : Effect() data class Success(val payload: Any) : Effect() data class Failed(val throwable: Throwable) : Effect() }™ class ActorImpl : Actor<State, Wish, Effect> { private val service: () -> Single<Any> = TODO() override fun invoke(state: State, wish: Wish): Observable<out Effect> = when (wish) { is LoadSomething -> service.invoke() .toObservable() .map { Success(it) as Effect } .startWith(Observable.just(StartedLoading)) .onErrorReturn { Failed(it) } }£ }º R Fe Ac
  10. R Fe Ac data class State( val isLoading: Boolean =

    false, valªhasError:ªBoolean = false, val payload: Any? = null )å class ReducerImpl : Reducer<State, Effect> { override fun invoke(state: State, effect: Effect): State = when (effect) { is Failed -> state.copy( isLoading = false, hasError = true ) valªhasError:ªBoolean )å is Failed -> state.copy( hasError = true )
  11. class AsyncFeature : ActorReducerFeature<Wish, Effect, State, News>( initialState = State(),

    actor = ActorImpl(), reducer = ReducerImpl(), newsPublisher = NewsPublisherImpl() )π{ sealed class News { data class NetworkRequestFailed(val throwable: Throwable) : News() }£ class NewsPublisherImpl : NewsPublisher<Wish, Effect, State, News> { override fun invoke(wish: Wish, effect: Effect, state: State): News? = when (effect) { is Failed -> News.NetworkRequestFailed( effect.throwable ) else -> null }™ }¡ }º F N R A W S E N
  12. F V Y X Pros Easy Cons No single-source of

    truth Connecting components Data-flow
  13. Pros Single source of truth! Better control over flow F

    V Y X Connecting components Data-flow
  14. fun connect() { val source = Observable.just(1, 2, 3) val

    consumer = Consumer<Int> { Log.d("New number!", it.toString()) }¢ val disposable = source.subscribe(consumer) }ƒ
  15. package io.reactivex; /** * Represents a basic, non-backpressured {@link Observab

    * consumable via an {@link Observer}. * * @param <T> the element type * @since 2.0 */ public interface ObservableSource<T> { /** * Subscribes the given Observer to this ObservableS * @param observer the Observer, not null * @throws NullPointerException if {@code observer} */ void subscribe(@NonNull Observer<? super T> observer); }
  16. package io.reactivex.functions; /** * A functional interface (callback) that accepts

    a singl * @param <T> the value type */ public interface Consumer<T> { /** * Consume the given value. * @param t the value * @throws Exception on error */ void accept(T t) throws Exception; }
  17. interface Feature : Consumer<Wish>, ObservableSource<State> { sealed class Wish data

    class State }£ interface View : Consumer<State>, ObservableSource<Wish> { }¡
  18. interface Feature : Consumer<Wish>, ObservableSource<State> { sealed class Wish data

    class State } interface View : Consumer<State>, ObservableSource<Wish> { }
  19. interface Feature : Consumer<Wish>, ObservableSource<State> { sealed class Wish data

    class State } interface View : Consumer<ViewModel>, ObservableSource<View.Event> { data class ViewModel sealed class Event }
  20. /** * Gluing layer between UI and Feature */ interface

    Boundary<in UiEvent : Any, ViewModel : Any> : Disposable { fun onUiEvent(uiEvent: UiEvent) val viewModels: Observable<ViewModel> } Connecting components Data-flow Coupling
  21. Pros No coupling :) Cons Not flexible :( F V

    F ??? Connecting components Data-flow Coupling
  22. Pros No coupling :) Cons Not flexible :( F F

    ??? Connecting components Data-flow Coupling
  23. Pros No coupling :) Cons Not flexible :( F V

    X Y ??? Connecting components Data-flow Coupling
  24. val output: ObservableSource<String> = Observable.just( "item1", "item2", "item3" )£ val

    input: Consumer<String> = Consumer { System.out.println(it) }£ val disposable = Observable .wrap(output) .subscribe(input)
  25. val output: ObservableSource<String> = Observable.just( "item1", "item2", "item3" )£ val

    input: Consumer<String> = Consumer { System.out.println(it) }£ val binder = Binder() binder.bind(output to input)
  26. val output: ObservableSource<String> = Observable.just( "item1", "item2", "item3" )£ val

    input: Consumer<Int> = Consumer { System.out.println(it) }£ val transformer : (String) -> Int = { it.length }£ val binder = Binder() binder.bind(output to input using transformer)
  27. binder.bind(view to feature using EventToWish)£ binder.bind(feature to view using StateToViewModel)

    binder.bind(feature to greenX using StateToGX) binder.bind(feature to greenY using StateToGY)
  28. binder.bind(view to analyticsTracker) binder.bind(view to feature using EventToWish) binder.bind(feature to

    view using StateToViewModel) binder.bind(feature to greenX using StateToGX) binder.bind(feature to greenY using StateToGY) binder.bind(view to analyticsTracker)
  29. val lifecycle: Lifecycle = TODO() val binder = Binder(lifecycle)£ binder.bind(view

    to analyticsTracker) binder.bind(view to feature using EventToWish) binder.bind(feature to view using StateToViewModel) binder.bind(feature to greenX using StateToGX) binder.bind(feature to greenY using StateToGY)
  30. C S Lifecycle sources Android / Create-Destroy Android / Start-Stop

    Android / Resume-Pause From Observable Manual Connecting components Data-flow Coupling Scoping
  31. binder.bind(view to analyticsTracker) binder.bind(view to feature using EventToWish) binder.bind(feature to

    view using StateToViewModel) binder.bind(feature to greenX using StateToGX) binder.bind(feature to greenY using StateToGY)
  32. package io.reactivex.functions; /** * A functional interface (callback) that accepts

    a singl * @param <T> the value type */ public interface Consumer<T> { /** * Consume the given value. * @param t the value * @throws Exception on error */ void accept(T t) throws Exception; }
  33. abstract class Middleware<T>( private val wrapped: Consumer<T> ) : Consumer<T>¡{

    override fun accept(t: T) { // do whatever here wrapped.accept(t) // do whatever here }& }º
  34. abstract class Middleware<T>( private val wrapped: Consumer<T> ) : Consumer<T>¡{

    override fun accept(t: T) { // Log it! wrapped.accept(t) }& }º
  35. abstract class Middleware<T>( private val wrapped: Consumer<T> ) : Consumer<T>¡{

    override fun accept(t: T) { // Save it somewhere for later! wrapped.accept(t) }& }º
  36. 1. Simple state reducing 2. Side-effects 3. Events 5. Coupling

    6. Scoping 7. Middlewares https://github.com/badoo/MVICore
  37. Additional resources GitHub project https://github.com/badoo/MVICore Article about Features https://badootech.badoo.com/a-modern-kotlin-based-mvi- architecture-9924e08efab1

    Article about Binder https://badootech.badoo.com/building-a-system-of-reactive- components-with-kotlin-ff56981e92cf Time Travel Debugger Demo https://badootech.badoo.com/time-travel-debug-everything- droidconuk-2018-lightning-talk-445217258401