@get:Bindable var title: String by binding("", BR.title) @get:Bindable var price: String by binding("", BR.price) ... interface Input { val articleId: PublishRelay<String> }ㅤ interface Output { val articleNotFound: PublishRelay<Unit> val articleDetailsUIState: BehaviorRelay<ArticleDetailsUIState> }ㅤ val input = object : Input { override val articleId = PublishRelay.create<String>() } val output = object : Output { override val articleNotFound = PublishRelay.create<Unit>() override val state = BehaviorRelay.create<ArticleDetailsUIState>() } override fun onViewResumed() { super.onViewResumed() input.articleId .flatMap { /* Get stuff from network */ } .observeOn(schedulerProvider.mainThread()) .subscribeOn(schedulerProvider.io()) .subscribe( { data -> // do things title = "Title" output.state.accept(aState) }, { error -> output.hideTopBanner.accept(Unit) }, ) .disposeOnPause() } } Data Binding 💀 • UI State in bindable properties • Custom binding adapters • Business Logic in the XML • Hard to test • Random tooling issues • KAPT @marcoGomier @stewemetal
@get:Bindable var title: String by binding("", BR.title) @get:Bindable var price: String by binding("", BR.price) ... interface Input { val articleId: PublishRelay<String> }ㅤ interface Output { val articleNotFound: PublishRelay<Unit> val articleDetailsUIState: BehaviorRelay<ArticleDetailsUIState> }ㅤ val input = object : Input { override val articleId = PublishRelay.create<String>() } val output = object : Output { override val articleNotFound = PublishRelay.create<Unit>() override val state = BehaviorRelay.create<ArticleDetailsUIState>() } override fun onViewResumed() { super.onViewResumed() input.articleId .flatMap { /* Get stuff from network */ } .observeOn(schedulerProvider.mainThread()) .subscribeOn(schedulerProvider.io()) .subscribe( { data -> // do things
var price: String by binding("", BR.price) ... interface Input { val articleId: PublishRelay<String> }ㅤ interface Output { val articleNotFound: PublishRelay<Unit> val articleDetailsUIState: BehaviorRelay<ArticleDetailsUIState> }ㅤ val input = object : Input { override val articleId = PublishRelay.create<String>() } val output = object : Output { override val articleNotFound = PublishRelay.create<Unit>() override val state = BehaviorRelay.create<ArticleDetailsUIState>() } override fun onViewResumed() { super.onViewResumed() input.articleId .flatMap { /* Get stuff from network */ } .observeOn(schedulerProvider.mainThread()) .subscribeOn(schedulerProvider.io()) .subscribe( { data -> // do things title = "Title" output.state.accept(aState) }, { error -> output.hideTopBanner.accept(Unit) }, ) .disposeOnPause() }ㅤ } • UI State split into multiple places • Rx specific boilerplate all over the place @marcoGomier @stewemetal
internal class ArticleDetailsViewModel( ... ) : BaseViewModel() { @get:Bindable var title: String by binding("", BR.title) @get:Bindable var price: String by binding("", BR.price) val input = object : Input { override val articleId = PublishRelay.create<String>() } val output = object : Output { override val articleNotFound = PublishRelay.create<Unit>() override val state = BehaviorRelay.create<ArticleDetailsUIState>() } }
@get:Bindable var title: String by binding("", BR.title) @get:Bindable var price: String by binding("", BR.price) val input = object : Input { override val articleId = PublishRelay.create<String>() } val output = object : Output { override val articleNotFound = PublishRelay.create<Unit>() override val state = BehaviorRelay.create<ArticleDetailsUIState>() } } Untangling the spaghetti - UI state, UI events
@get:Bindable var title: String by binding("", BR.title) @get:Bindable var price: String by binding("", BR.price) val input = object : Input { override val articleId = PublishRelay.create<String>() } val output = object : Output { override val articleNotFound = PublishRelay.create<Unit>() override val state = BehaviorRelay.create<ArticleDetailsUIState>() } } Untangling the spaghetti - UI state, UI events
abstract class BaseTierViewModel < Stat e >( val initialState: State, ) : ViewModel() { private val state = BehaviorSubject.createDefault(initialState) ... }
abstract class BaseTierViewModel < Stat e >( val initialState: State, ) : ViewModel() { private val state = BehaviorSubject.createDefault(initialState) fun state(): Observable<State> = state.hide() ... }
abstract class BaseTierViewModel<State>( val initialState: State, ) : ViewModel() { private val state = BehaviorSubject.createDefault(initialState) fun state(): Observable<State> = state.hide() ... } sealed class ArticleDetailsUIState { object Loading: ArticleDetailsUIState() data class Content( val title: String, val price: String, val articleDetailsEvent: ArticleDetailsEvent? = null, ): ArticleDetailsUIState() }
abstract class BaseTierViewModel<State>( val initialState: State, ) : ViewModel() { private val state = BehaviorSubject.createDefault(initialState) fun state(): Observable<State> = state.hide() ... } data class ArticleDetailsUIState( val isLoading: Boolean = true, val title: String, val price: String, val articleDetailsEvent: ArticleDetailsEvent? = null, )
abstract class BaseTierViewModel<State>( val initialState: State, ) : ViewModel() { private val state = BehaviorSubject.createDefault(initialState) fun state(): Observable<State> = state.hide() ... }
Binding and @BindingAdapters • Good component APIs & docs • Easy usage with View-based UI • Not fun to maintain 😢 OctopusButtonPrimary.kt R.layout.__internal_view_octopus_button R.styleable.OctopusButton R.drawable.__internal_octopus_button_background_primary @marcoGomier @stewemetal
Binding and @BindingAdapters • Good component APIs & docs • Easy usage with View-based UI • Not fun to maintain 😢 // We have to disable specific setters to prevent misuse of the view private var areSettersEnabled = false @marcoGomier @stewemetal
functions • Take time to revisit your architecture • A design system will help • Jumping on the Compose Ship Scooter is not a race • Zero ➡ Hero takes time We’re still here 😄