diff --git a/README.md b/README.md index 42ebea1..acaf673 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,21 @@ # ReactiveViewModel -This is Android reactive MVVM library, fork https://github.com/dmdevgo/RxPM +Очередная библиотека Android Reactive MVVM, за основу взята https://github.com/dmdevgo/RxPM + +Основная цель библиотеки, это организовать направление данных **View** <-> **ViewModel** и **сокрытия** методов по изменению состояния напрямую из **View**. + +Пример такого сокрытия вы могли видеть (или применять) при использовании **LiveData** + +```kotlin +prvate val _state = MutableLiveData() +val state: LiveData = _state +``` + +Вместо этого, библиотека предлагает делать это [так](#пример-viewmodel) -The Status of the lib: [![](https://jitpack.io/v/AlexDeww/ReactiveViewModel.svg)](https://jitpack.io/#AlexDeww/ReactiveViewModel) -How to use this lib in your project: +Подключение: ```gradle allprojects { repositories { @@ -15,7 +25,6 @@ allprojects { } ``` -Add to your app module build.gradle ```gradle dependencies { implementation "com.github.AlexDeww:ReactiveViewModel:$last_version" @@ -24,147 +33,263 @@ dependencies { ### Пример ViewModel ```kotlin -class EnterSmsCodeViewModel( - val fullPhoneNumber: String, - private val requestSmsCode: RequestSmsCode, - private val registerOrSignIn: RegisterOrSignIn -): ReactiveViewModel() { +class SomeViewModel( + savedState: SavedStateHandle +) : ReactiveViewModel() { - private val eventCancelTimer = eventNone() + val progressVisible by RVM.progressState(initialValue = false) + val tryCount by savedState.state(initialValue = 5) + + val tryCountLabelVisible by RVM.stateProjection(tryCount) { it > 0 } + val sendButtonEnable by RVM.stateProjectionFromSource(initialValue = false) { + combineLatest( + progressVisible.observable, + tryCountLabelVisible.observable, + inputCode.data.observable.map { it.length >= 4 } + ) { isProgress, hasTryCount, codeReached -> !isProgress && hasTryCount && codeReached } + } - val progressVisibility = state(initValue, PROGRESS_DEBOUNCE_INTERVAL) - val timerVisibility = state(false) - val timerValue = state() - val blocked = state(false) + val inputCode by savedState.inputControl() - val inputCode = inputControl() + val eventDone by RVM.eventNone() + val eventError by RVM.event() - val eventWrongSmsCode = eventNone() - val eventGoToTripStart = eventNone() - val eventGoToAcceptPolicy = eventNone() - val eventCodeExpired = eventNone() + val actionOnSendCodeClick by RVM.debouncedActionNone() - val actionSendCodeAgainClick = debouncedActionNone() - - init { - inputCode.value.observable - .filter { blocked.value == false && progressVisibility.value == false } - .debounce() - .filter { it.length == SMS_CODE_LENGTH } - .doOnNext { analyticsManager.trackEvent(AppAnalyticEvents.ConfirmPhone) } - .switchMapSingle { register(it) } - .doOnError { inputCode.actionChangeValue.call("") } - .retry() - .subscribe { - when { - it -> eventGoToTripStart.call() - else -> eventGoToAcceptPolicy.call() - } + private val sendCode by RVM.invocable { code -> + Completable + .fromAction { + tryCount.valueNonNull.takeIf { it > 0 }?.let { tryCount.setValue(it - 1) } + Log.d("VM", "Code sent: $code") } - .disposeOnCleared() - - actionSendCodeAgainClick.observable - .filter { blocked.value == false && timerVisibility.value == false && progressVisibility.value == false } - .switchMapSingle { requestCode() } - .switchMap { getTimerObservable(it) } - .retry() - .subscribe() - .disposeOnCleared() - - getRequestSmsTimerValue.execute(Unit) - .takeIf { it > 0 } - ?.let { getTimerObservable(it) } - ?.subscribe() - ?.disposeOnCleared() + .delaySubscription(5, TimeUnit.SECONDS) + .bindProgress(progressVisible.consumer) + .doOnComplete { eventDone.call() } + .doOnError { eventError.call(it) } + } + + init { + actionOnSendCodeClick.bind { + this.filter { sendButtonEnable.value == true } + .doOnNext { sendCode(inputCode.data.valueNonNull) } + } } + } ``` ### Связь с View ```kotlin -class EnterSmsCodeFragment : ReactiveFragment() { +class SomeFragment : ReactiveFragment() { + + private val viewModel: SomeViewModel by Delegates.notNull() - companion object { - private const val ARG_FULL_PHONE_NUMBER = "EnterSmsCodeFragment.ARG_FULL_PHONE_NUMBER" - - fun create(fullPhoneNumber: String): EnterSmsCodeFragment = EnterSmsCodeFragment() - .args { putString(ARG_FULL_PHONE_NUMBER, fullPhoneNumber) } - } - - private val viewModel by viewModel { // koin!!! - parametersOf(arguments?.getString(ARG_FULL_PHONE_NUMBER)!!) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - - viewModel.progressVisibility.observe { if (it) showProgress() else hideProgress() } - viewModel.timerValue.observe { tvTimer.text = formatTimer(it) } - viewModel.timerVisibility.observe { tvTimer.isVisible = it } - - viewModel.eventError.observe { showError(it) } - viewModel.eventGoToTripStart.observe { router.newRootScreen(Screens.Main.TripSetupFlowScreen()) } - - viewModel.inputCode.bindTo(etSmsCode) - viewModel.actionSendCodeAgainClick.bindOnClick(btnSendAgain) + + viewModel.progressVisible.observe { progressView.isVisible = it } + viewModel.tryCount.observe { tryCountLabel.text = it.toString() } + viewModel.tryCountLabelVisible.observe { tryCountLabel.isVisible = it } + viewModel.sendButtonEnable.observe { sendButton.isEnabled = it } + + viewModel.inputCode.bindTo(codeEditText) + + viewModel.eventDone.observe { /* close fragment */ } + viewModel.eventError.observe { /* show error */ } + + viewModel.actionOnSendCodeClick.bindOnClick(sendButton) } - + +} +``` + +## Есть 5 базовых объектов для взаимодействия View и ViewModel + +### RvmState +В основном предназначен для передачи состояния из **ViewModel** во **View**. + - Передавать данные в **RvmState** могут только наследники **RvmPropertiesSupport**. + - Всегда хранит последнее переданное значение. + - Каждый подписчик в первую очередь получит последннее сохраненное значение. + +Объявление: + +```kotlin +val state by RVM.state(initialValue = null or data) +``` +```kotlin +val state by savedStateHandle.state(initialValue = null or data) +``` + +Подписка: + +```kotlin +viewModel.state.observe { value -> /* do */ } +``` + + +### RvmStateProjection +Почти тоже самое, что и **RvmState**, но отличается тем, что никто не может передавать данные напрямую. + - Никто не может передавать данные напрямую. **RvmStateProjection** может получать данные от источников: **Observable**, **RvmState**, **RvmStateProjection**, либо объекта наследника **RvmPropertyBase** и **RvmValueProperty**. + +Объявление: + +```kotlin +val state by RVM.state(initialValue = null or data) +val stateProjection by RVM.stateProjection(state) { /* map block */ } +``` +```kotlin +val stateProjection by RVM.stateProjectionFromSource(initialValue = null or data) { ObservableSource } +``` + +Подписка: + +```kotlin +viewModel.stateProjection.observe { value -> /* do */ } +``` + +### RvmEvent +В основном предназначен для передачи событий или данных из **ViewModel** во **View**. + - Передавать данные в **RvmEvent** могут только наследники **RvmPropertiesSupport**. + - Хранит последнее переданное значение пока не появится подписчик. Только первый подписчик получит последнее сохраненное значение, все последующие подписки, будут получать только новые значения. + - Пока есть активная подписка, данные не сохраняются. + +Объявление: + +```kotlin +val event by RVM.event() +``` +```kotlin +val event by RVM.eventNone() // for Unit Data Type +``` + +Подписка: + +```kotlin +viewModel.event.observe { value -> /* do */ } +``` + +### RvmConfirmationEvent +Почти тоже самое, что и **RvmEvent**, но отличается тем, что хранит последнее значение пока не будет вызван метод **confirm**. + - Передавать данные в **RvmConfirmationEvent** могут только наследники **RvmPropertiesSupport**. + - Хранит последнее переданное значение пока не будет вызван метод **confirm**. Каждый новый подписчик будет получать последнее сохраненное значение, пока не вызван метод **confirm**. + +Объявление: + +```kotlin +val confirmationEvent by RVM.confirmationEvent() +``` +```kotlin +val confirmationEvent by RVM.confirmationEventNone() // for Unit Data Type +``` + +Подписка: + +```kotlin +viewModel.confirmationEvent.observe { value -> + /* do */ + viewModel.confirmationEvent.confirm() } ``` -### State -**State** хранит послдение значение и излучает его при подписке. Используется для передачи значения из ViewModel в View +### RvmAction +В основном предназначен для передачи событий или данных из **View** во **ViewModel**. + - Не хранит данные. + +Объявление: -Создание ```kotlin -val isProgress = state(false) +val action by RVM.action() ``` -Из ViewModel ```kotlin -isProgress.consumer.accept(true) -isProgress.setValue(true) // расширение для isProgress.consumer.accept(true) -isProgress.setValueIfChanged(true) // расширение для isProgress.consumer.accept(true) но с проверкой if (lastValue != newValue) +val action by RVM.actionNone() // for Unit Data Type ``` -В View + +Привязка: + ```kotlin -isProgress.observe { value -> } +viewModel.action.bindOnClick(someView) ``` -### Action -**Action** ипользуется для передачи событий или параметров из View в ViewModel +## Также имеется 4 вспомогательных объекта для удобной связи View-элементов с ViewModel + +### RvmCheckControl +Для связи **CompoundButton** и **ViewModel**. + +Объявление: -Создание ```kotlin -val actionSendSmsCodeAgain = action() // or actionEmpty() если тип Unit +val checkControl by RVM.checkControl(initialChecked = false) ``` -Из ViewModel ```kotlin -actionSendSmsCodeAgain.consumer.accept(Unit) -actionSendSmsCodeAgain.call() // расширение для actionSendSmsCodeAgain.consumer.accept(Unit) +val checkControl by savedStateHandle.checkControl(initialChecked = false) ``` -В View + +Привязка: + ```kotlin -actionSendSmsCodeAgain.bindOnClick(btnSendSmsCode) -btnSendSmsCode.setOnClickListener { actionSendSmsCodeAgain.call() } +viewModel.checkControl.bindTo(compoundButton) ``` -### Event -**Event** ипользуется для передачи событий или параметров из ViewModel в View. Хранит последнее переданное значение, пока не появится подписчик. +### RvmRatingControl +Для связи **RatingBar** и **ViewModel**. + +Объявление: -Создание ```kotlin -val eventDone = event() // or eventEmpty() если тип Unit +val ratingControl by RVM.ratingControl(initialValue = 3) ``` -Из ViewModel ```kotlin -eventDone.consumer.accept(Unit) -eventDone.call() // расширение для eventDone.consumer.accept(Unit) +val ratingControl by savedStateHandle.ratingControl(initialValue = 3) ``` -В View + +Привязка: + ```kotlin -eventDone.observe { value -> } +viewModel.ratingControl.bindTo(ratingBar) ``` +### RvmInputControl +Для связи **EditText** или **TextInputLayout** и **ViewModel**. -## Инфо -Вся либа, надстройка над LiveData. Cвойства(state, event) имею поле **liveData** для возможности совместного использования с **DataBinding** +Объявление: + +```kotlin +val inputControl by RVM.inputControl(initialText = "Some Text") +``` +```kotlin +val inputControl by savedStateHandle.inputControl(initialText = "Some Text") +``` + +Привязка: + +```kotlin +viewModel.inputControl.bindTo(editText) +``` +```kotlin +viewModel.inputControl.bindTo(textInputLayout) +``` + +### RvmDialogControl +Для связи **Dialog** и **ViewModel**. + +Объявление: + +```kotlin +val dialogControl by RVM.dialogControl() +``` +```kotlin +val dialogControl by RVM.dialogControlDefaultResult() +``` + +Привязка: + +```kotlin +viewModel.dialogControl.bindTo { data: DataType, dc: RvmDialogControlResult -> + MaterialAlertDialogBuilder(context) + .title("Title") + .message("Message") + .setPositiveButton("OK") { _, _ -> dc.sendResult(ResultDataType) } + .setNegativeButton("Cancel") { _, _ -> dc.sendResult(ResultDataType) } + .create() +} +``` diff --git a/build.gradle b/build.gradle index 1527b73..80d8b90 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { min_sdk_version = 17 sdk_version = 31 - kotlin_version = '1.7.0' + kotlin_version = '1.7.21' rxjava_version = '3.1.2' rxandroid_version = '3.0.0' archx_version = '2.2.0' diff --git a/reactiveviewmodel/build.gradle b/reactiveviewmodel/build.gradle index 0b195ff..3990958 100644 --- a/reactiveviewmodel/build.gradle +++ b/reactiveviewmodel/build.gradle @@ -45,7 +45,7 @@ afterEvaluate { release(MavenPublication) { from components.release groupId = 'com.alexdeww.reactiveviewmodel' - version = '2.5.1' + version = '3.0.1' } } } diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/component/ReactiveActivity.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/component/ReactiveActivity.kt index a96d7fa..5751dca 100644 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/component/ReactiveActivity.kt +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/component/ReactiveActivity.kt @@ -3,41 +3,36 @@ package com.alexdeww.reactiveviewmodel.component import androidx.annotation.CallSuper import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.LifecycleOwner +import com.alexdeww.reactiveviewmodel.core.DefaultRvmDisposableStore +import com.alexdeww.reactiveviewmodel.core.RvmAutoDisposableSupport import com.alexdeww.reactiveviewmodel.core.RvmViewComponent +import com.alexdeww.reactiveviewmodel.widget.RvmWidgetBindShortcut import io.reactivex.rxjava3.disposables.Disposable -abstract class ReactiveActivity : AppCompatActivity(), RvmViewComponent { +abstract class ReactiveActivity : AppCompatActivity(), RvmViewComponent, RvmWidgetBindShortcut { - private val disposableOnDestroyList = HashMap() - private val disposableOnStopList = HashMap() + private val rvmAutoDisposableStore by lazy { DefaultRvmDisposableStore() } + final override val componentLifecycleOwner: LifecycleOwner get() = this - override val componentLifecycleOwner: LifecycleOwner - get() = this + final override fun Disposable.autoDispose( + tagKey: String, + storeKey: RvmAutoDisposableSupport.StoreKey? + ) = rvmAutoDisposableStore.run { this@autoDispose.autoDispose(tagKey, storeKey) } @CallSuper override fun onStop() { - disposableOnStopList.values.forEach { it.dispose() } - disposableOnStopList.clear() + rvmAutoDisposableStore.dispose(RvmViewComponent.onStopStoreKey) super.onStop() } @CallSuper override fun onDestroy() { - disposableOnDestroyList.values.forEach { it.dispose() } - disposableOnDestroyList.clear() + rvmAutoDisposableStore.dispose() super.onDestroy() } - override fun Disposable.disposeOnDestroy(tag: String) { - disposableOnDestroyList.put(tag, this)?.dispose() - } - - override fun Disposable.disposeOnStop(tag: String) { - disposableOnStopList.put(tag, this)?.dispose() - } - override fun Disposable.disposeOnDestroyView(tag: String) { - disposableOnDestroyList.put("dv-$tag", this)?.dispose() + autoDispose("dv-$tag", RvmViewComponent.onDestroyViewStoreKey) } -} \ No newline at end of file +} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/component/ReactiveFragment.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/component/ReactiveFragment.kt index 11869ca..b7cefd6 100644 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/component/ReactiveFragment.kt +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/component/ReactiveFragment.kt @@ -4,53 +4,40 @@ import androidx.annotation.CallSuper import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner +import com.alexdeww.reactiveviewmodel.core.DefaultRvmDisposableStore +import com.alexdeww.reactiveviewmodel.core.RvmAutoDisposableSupport import com.alexdeww.reactiveviewmodel.core.RvmViewComponent +import com.alexdeww.reactiveviewmodel.widget.RvmWidgetBindShortcut import io.reactivex.rxjava3.disposables.Disposable -abstract class ReactiveFragment : Fragment, RvmViewComponent { +abstract class ReactiveFragment( + @LayoutRes layoutId: Int = 0 +) : Fragment(layoutId), RvmViewComponent, RvmWidgetBindShortcut { - constructor() : super() + private val rvmAutoDisposableStore by lazy { DefaultRvmDisposableStore() } + final override val componentLifecycleOwner: LifecycleOwner get() = viewLifecycleOwner - constructor(@LayoutRes layoutId: Int) : super(layoutId) - - private val disposableOnDestroyList = HashMap() - private val disposableOnStopList = HashMap() - private val disposableOnDestroyViewList = HashMap() - - override val componentLifecycleOwner: LifecycleOwner - get() = viewLifecycleOwner + final override fun Disposable.autoDispose( + tagKey: String, + storeKey: RvmAutoDisposableSupport.StoreKey? + ) = rvmAutoDisposableStore.run { this@autoDispose.autoDispose(tagKey, storeKey) } @CallSuper override fun onStop() { - disposableOnStopList.values.forEach { it.dispose() } - disposableOnStopList.clear() + rvmAutoDisposableStore.dispose(RvmViewComponent.onStopStoreKey) super.onStop() } @CallSuper override fun onDestroyView() { - disposableOnDestroyViewList.values.forEach { it.dispose() } - disposableOnDestroyViewList.clear() + rvmAutoDisposableStore.dispose(RvmViewComponent.onDestroyViewStoreKey) super.onDestroyView() } @CallSuper override fun onDestroy() { - disposableOnDestroyList.values.forEach { it.dispose() } - disposableOnDestroyList.clear() + rvmAutoDisposableStore.dispose() super.onDestroy() } - override fun Disposable.disposeOnDestroy(tag: String) { - disposableOnDestroyList.put(tag, this)?.dispose() - } - - override fun Disposable.disposeOnStop(tag: String) { - disposableOnStopList.put(tag, this)?.dispose() - } - - override fun Disposable.disposeOnDestroyView(tag: String) { - disposableOnDestroyViewList.put(tag, this)?.dispose() - } - } diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/component/ReactiveViewModel.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/component/ReactiveViewModel.kt new file mode 100644 index 0000000..27b1b02 --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/component/ReactiveViewModel.kt @@ -0,0 +1,81 @@ +package com.alexdeww.reactiveviewmodel.component + +import androidx.annotation.CallSuper +import androidx.lifecycle.ViewModel +import com.alexdeww.reactiveviewmodel.core.DefaultRvmDisposableStore +import com.alexdeww.reactiveviewmodel.core.RvmAutoDisposableSupport +import com.alexdeww.reactiveviewmodel.core.RvmViewModelComponent +import com.alexdeww.reactiveviewmodel.core.annotation.RvmBinderDslMarker +import com.alexdeww.reactiveviewmodel.core.property.* +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.functions.Consumer + +/** + * Based on RxPM + * https://github.com/dmdevgo/RxPM + */ + +abstract class ReactiveViewModel : ViewModel(), RvmViewModelComponent { + + private val rvmAutoDisposableStore by lazy { DefaultRvmDisposableStore() } + private val defaultViewModelComponent = object : RvmViewModelComponent { + override fun Disposable.autoDispose( + tagKey: String, + storeKey: RvmAutoDisposableSupport.StoreKey? + ) = this@ReactiveViewModel.rvmAutoDisposableStore.run { + this@autoDispose.autoDispose(tagKey, storeKey) + } + } + + @CallSuper + override fun onCleared() { + rvmAutoDisposableStore.dispose() + super.onCleared() + } + + final override fun Disposable.autoDispose( + tagKey: String, + storeKey: RvmAutoDisposableSupport.StoreKey? + ) = defaultViewModelComponent.run { autoDispose(tagKey, storeKey) } + + final override val RvmProperty.consumer: Consumer + get() = defaultViewModelComponent.run { consumer } + final override val RvmPropertyBase.observable: Observable + get() = defaultViewModelComponent.run { observable } + + final override fun R.call(value: T) where R : RvmCallableProperty, + R : RvmProperty { + defaultViewModelComponent.run { call(value) } + } + + final override fun R.call() where R : RvmCallableProperty, + R : RvmProperty { + defaultViewModelComponent.run { call() } + } + + final override fun R.setValue(value: T) where R : RvmMutableValueProperty, + R : RvmProperty { + defaultViewModelComponent.run { setValue(value) } + } + + final override fun R.setValueIfChanged(value: T) where R : RvmMutableValueProperty, + R : RvmProperty { + defaultViewModelComponent.run { setValueIfChanged(value) } + } + + @RvmBinderDslMarker + final override fun RvmAction.bind( + chainBlock: Observable.() -> Observable + ) { + defaultViewModelComponent.run { bind(chainBlock) } + } + + @RvmBinderDslMarker + final override fun RvmState.bind( + chainBlock: Observable.() -> Observable + ) { + defaultViewModelComponent.run { bind(chainBlock) } + } + +} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/Const.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/Const.kt new file mode 100644 index 0000000..f2aa7d4 --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/Const.kt @@ -0,0 +1,4 @@ +package com.alexdeww.reactiveviewmodel.core + +const val DEF_PROGRESS_DEBOUNCE_INTERVAL = 500L //ms +const val DEF_ACTION_DEBOUNCE_INTERVAL = 300L //ms diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RVM.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RVM.kt new file mode 100644 index 0000000..49535c7 --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RVM.kt @@ -0,0 +1,3 @@ +package com.alexdeww.reactiveviewmodel.core + +object RVM diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/ReactiveViewModel.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/ReactiveViewModel.kt deleted file mode 100644 index 4e134e6..0000000 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/ReactiveViewModel.kt +++ /dev/null @@ -1,150 +0,0 @@ -package com.alexdeww.reactiveviewmodel.core - -import androidx.lifecycle.ViewModel -import com.alexdeww.reactiveviewmodel.core.common.RvmComponent -import com.alexdeww.reactiveviewmodel.core.property.Action -import com.alexdeww.reactiveviewmodel.core.property.ConfirmationEvent -import com.alexdeww.reactiveviewmodel.core.property.Event -import com.alexdeww.reactiveviewmodel.core.property.State -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Maybe -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.subjects.BehaviorSubject - -/** - * Based on RxPM - * https://github.com/dmdevgo/RxPM - */ - -const val DEF_PROGRESS_DEBOUNCE_INTERVAL = 500L //ms -const val DEF_ACTION_DEBOUNCE_INTERVAL = 300L //ms - -abstract class ReactiveViewModel : ViewModel(), RvmComponent { - - protected interface Invocable { - val isExecute: Boolean - val isExecuteObservable: Observable - operator fun invoke(params: T) - } - - private val disposableList = CompositeDisposable() - - override fun onCleared() { - disposableList.clear() - super.onCleared() - } - - fun Disposable.disposeOnCleared(): Disposable { - disposableList.add(this) - return this - } - - protected fun state( - initValue: T? = null, - debounceInterval: Long? = null - ): State = State(initValue, debounceInterval) - - protected fun progressState( - initValue: Boolean? = null, - debounceInterval: Long = DEF_PROGRESS_DEBOUNCE_INTERVAL - ): State = state(initValue, debounceInterval) - - protected fun event(debounceInterval: Long? = null): Event = - Event(debounceInterval) - - protected fun eventNone(debounceInterval: Long? = null): Event = - event(debounceInterval) - - protected fun confirmationEvent( - debounceInterval: Long? = null - ): ConfirmationEvent = ConfirmationEvent(debounceInterval) - - protected fun confirmationEventNone( - debounceInterval: Long? = null - ): ConfirmationEvent = confirmationEvent(debounceInterval) - - protected fun action(debounceInterval: Long? = null): Action = - Action(debounceInterval) - - protected fun actionNone(debounceInterval: Long? = null): Action = - action(debounceInterval) - - protected fun debouncedAction( - debounceInterval: Long = DEF_ACTION_DEBOUNCE_INTERVAL - ): Action = action(debounceInterval) - - protected fun debouncedActionNone( - debounceInterval: Long = DEF_ACTION_DEBOUNCE_INTERVAL - ): Action = action(debounceInterval) - - protected fun Action.bind( - transformChainBlock: Observable.() -> Observable - ) { - observable - .transformChainBlock() - .applyDefaultErrorHandler() - .retry() - .subscribe() - .disposeOnCleared() - } - - protected fun State.bind( - transformChainBlock: Observable.() -> Observable - ) { - observable - .transformChainBlock() - .applyDefaultErrorHandler() - .retry() - .subscribe() - .disposeOnCleared() - } - - protected fun invocable( - block: (params: T) -> Completable - ): Lazy> = lazy { - val action = action() - val isExecuteSubj: BehaviorSubject = BehaviorSubject.createDefault(false) - action.bind { - this.switchMapCompletable { params -> block(params).bindProgress(isExecuteSubj::onNext) } - .toObservable() - } - object : Invocable { - override val isExecute: Boolean get() = isExecuteSubj.value ?: false - override val isExecuteObservable: Observable = isExecuteSubj.serialize() - - override fun invoke(params: T) = action.consumer.accept(params) - } - } - - protected fun Observable.untilOn(vararg action: Action): Observable = - takeUntil(Observable.merge(action.map { it.observable })) - - protected fun Observable.untilOn(vararg event: Event): Observable = - takeUntil(Observable.merge(event.map { it.observable })) - - protected fun Observable.untilOn(vararg observable: Observable<*>): Observable = - takeUntil(Observable.merge(observable.toList())) - - protected fun Maybe.untilOn(vararg action: Action<*>): Maybe = - takeUntil(Maybe.merge(action.map { it.observable.firstElement() })) - - protected fun Maybe.untilOn(vararg event: Event<*>): Maybe = - takeUntil(Maybe.merge(event.map { it.observable.firstElement() })) - - protected fun Maybe.untilOn(vararg maybe: Maybe<*>): Maybe = - takeUntil(Maybe.merge(maybe.toList())) - - protected fun Completable.untilOn(vararg action: Action<*>): Completable = - takeUntil(Completable.merge(action.map { it.observable.firstElement().ignoreElement() })) - - protected fun Completable.untilOn(vararg event: Event<*>): Completable = - takeUntil(Completable.merge(event.map { it.observable.firstElement().ignoreElement() })) - - protected fun Completable.untilOn(vararg completable: Completable): Completable = - takeUntil(Completable.merge(completable.toList())) - - protected open fun Observable.applyDefaultErrorHandler(): Observable = this - -} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmAutoDisposableSupport.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmAutoDisposableSupport.kt new file mode 100644 index 0000000..f5c4555 --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmAutoDisposableSupport.kt @@ -0,0 +1,48 @@ +package com.alexdeww.reactiveviewmodel.core + +import io.reactivex.rxjava3.disposables.Disposable +import java.util.* + +interface RvmAutoDisposableSupport { + + data class StoreKey(val name: String) { + init { + check(name.isNotBlank()) { "Name can`t be blank" } + } + } + + fun Disposable.autoDispose( + tagKey: String = UUID.randomUUID().toString(), + storeKey: StoreKey? = null + ) + +} + +interface RvmAutoDisposableStore : RvmAutoDisposableSupport { + fun dispose(storeKey: RvmAutoDisposableSupport.StoreKey? = null) +} + +class DefaultRvmDisposableStore : RvmAutoDisposableStore { + + private val disposablesStore = + hashMapOf>() + + override fun dispose(storeKey: RvmAutoDisposableSupport.StoreKey?) { + if (storeKey == null) { + disposablesStore.entries.forEach { it.value.disposeAndClear() } + disposablesStore.clear() + } else { + disposablesStore[storeKey]?.disposeAndClear() + } + } + + override fun Disposable.autoDispose(tagKey: String, storeKey: RvmAutoDisposableSupport.StoreKey?) { + disposablesStore.getOrPut(storeKey) { hashMapOf() }.put(tagKey, this)?.dispose() + } + + private fun HashMap.disposeAndClear() { + values.forEach { it.dispose() } + clear() + } + +} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmExtensions.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmExtensions.kt index 89ec2d4..f0c524c 100644 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmExtensions.kt +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmExtensions.kt @@ -4,10 +4,9 @@ import android.view.View import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import com.alexdeww.reactiveviewmodel.core.property.Action -import com.alexdeww.reactiveviewmodel.core.property.ConfirmationEvent -import com.alexdeww.reactiveviewmodel.core.property.Event -import com.alexdeww.reactiveviewmodel.core.property.State +import com.alexdeww.reactiveviewmodel.core.property.RvmAction +import com.alexdeww.reactiveviewmodel.core.property.RvmObservableProperty +import com.alexdeww.reactiveviewmodel.core.property.RvmPropertyBase import io.reactivex.rxjava3.core.* import io.reactivex.rxjava3.functions.Consumer import io.reactivex.rxjava3.functions.Function @@ -21,33 +20,23 @@ fun LiveData.observe(owner: LifecycleOwner, action: OnLiveDataAction): return observer } -fun Event.observe( +fun RvmObservableProperty.observe( owner: LifecycleOwner, action: OnLiveDataAction ): Observer = liveData.observe(owner = owner, action = action) -fun ConfirmationEvent.observe( - owner: LifecycleOwner, - action: OnLiveDataAction -): Observer = liveData.observe(owner = owner, action = action) - -fun State.observe( - owner: LifecycleOwner, - action: OnLiveDataAction -): Observer = liveData.observe(owner = owner, action = action) - -fun Action.call() = call(Unit) +fun RvmAction.call() = call(Unit) typealias ActionOnClick = () -> Unit -fun Action.bindOnClick(view: View, value: T, onClickAction: ActionOnClick? = null) { +fun RvmAction.bindOnClick(view: View, value: T, onClickAction: ActionOnClick? = null) { view.setOnClickListener { call(value) onClickAction?.invoke() } } -fun Action.bindOnClick(view: View, onClickAction: ActionOnClick? = null) { +fun RvmAction.bindOnClick(view: View, onClickAction: ActionOnClick? = null) { bindOnClick(view, Unit, onClickAction) } @@ -131,6 +120,32 @@ fun Completable.bindProgressAny(progressConsumer: Consumer): Completabl .doOnDispose { consumerWrapper.end() } } +fun Observable.untilOn( + vararg rvmProperty: RvmPropertyBase<*> +): Observable = takeUntil(Observable.merge(rvmProperty.map { it.observable })) + +fun Observable.untilOn( + vararg observable: Observable<*> +): Observable = takeUntil(Observable.merge(observable.toList())) + +fun Maybe.untilOn( + vararg rvmProperty: RvmPropertyBase<*> +): Maybe = takeUntil(Maybe.merge(rvmProperty.map { it.observable.firstElement() })) + +fun Maybe.untilOn( + vararg maybe: Maybe<*> +): Maybe = takeUntil(Maybe.merge(maybe.toList())) + +fun Completable.untilOn( + vararg rvmProperty: RvmPropertyBase<*> +): Completable = takeUntil(Completable.merge(rvmProperty.map { + it.observable.firstElement().ignoreElement() +})) + +fun Completable.untilOn( + vararg completable: Completable +): Completable = takeUntil(Completable.merge(completable.toList())) + /** * Returns the [Observable] that emits items when active, and buffers them when [idle][isIdle]. * Buffered items is emitted when idle state ends. diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmPropertiesSupport.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmPropertiesSupport.kt new file mode 100644 index 0000000..c035d40 --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmPropertiesSupport.kt @@ -0,0 +1,137 @@ +package com.alexdeww.reactiveviewmodel.core + +import com.alexdeww.reactiveviewmodel.core.annotation.RvmDslMarker +import com.alexdeww.reactiveviewmodel.core.property.* +import com.alexdeww.reactiveviewmodel.core.utils.RvmPropertyDelegate +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.functions.Consumer +import kotlin.properties.ReadOnlyProperty + +@RvmDslMarker +interface RvmPropertiesSupport { + + // common + val RvmProperty.consumer: Consumer get() = this.consumer + val RvmPropertyBase.observable: Observable get() = this.observable + + + // callable property + fun R.call(value: T) where R : RvmCallableProperty, + R : RvmProperty = consumer.accept(value) + + fun R.call() where R : RvmCallableProperty, + R : RvmProperty = call(Unit) + + + // mutable property + fun R.setValue(value: T) where R : RvmMutableValueProperty, + R : RvmProperty = consumer.accept(value) + + fun R.setValueIfChanged(value: T) where R : RvmMutableValueProperty, + R : RvmProperty { + if (this.value != value) setValue(value) + } + +} + +@Suppress("unused") +@RvmDslMarker +fun RVM.state( + initialValue: T? = null, + debounceInterval: Long? = null +): ReadOnlyProperty> = RvmPropertyDelegate.def { + RvmState(initialValue, debounceInterval) +} + +@Suppress("unused") +@RvmDslMarker +fun RVM.stateProjection( + stateSource: P, + distinctUntilChanged: Boolean = true, + mapBlock: (value: T) -> R +): ReadOnlyProperty> where P : RvmPropertyBase, + P : RvmValueProperty { + return RvmPropertyDelegate.def { + val projection = RvmStateProjection() + val d = stateSource.observable + .map(mapBlock) + .run { if (distinctUntilChanged) distinctUntilChanged() else this } + .subscribe(projection.consumer) + if (this is RvmAutoDisposableSupport) d.autoDispose() + projection + } +} + +@Suppress("unused") +@RvmDslMarker +fun RVM.progressState( + initialValue: Boolean? = null, + debounceInterval: Long = DEF_PROGRESS_DEBOUNCE_INTERVAL +): ReadOnlyProperty> = state( + initialValue = initialValue, + debounceInterval = debounceInterval +) + +@Suppress("unused") +@RvmDslMarker +fun RVM.event( + debounceInterval: Long? = null +): ReadOnlyProperty> = RvmPropertyDelegate.def { + RvmEvent(debounceInterval) +} + +@Suppress("unused") +@RvmDslMarker +fun RVM.eventNone( + debounceInterval: Long? = null +): ReadOnlyProperty> = event( + debounceInterval = debounceInterval +) + +@Suppress("unused") +@RvmDslMarker +fun RVM.confirmationEvent( + debounceInterval: Long? = null +): ReadOnlyProperty> = RvmPropertyDelegate.def { + RvmConfirmationEvent(debounceInterval) +} + +@Suppress("unused") +@RvmDslMarker +fun RVM.confirmationEventNone( + debounceInterval: Long? = null +): ReadOnlyProperty> = confirmationEvent( + debounceInterval = debounceInterval +) + +@Suppress("unused") +@RvmDslMarker +fun RVM.action( + debounceInterval: Long? = null +): ReadOnlyProperty> = RvmPropertyDelegate.def { + RvmAction(debounceInterval) +} + +@Suppress("unused") +@RvmDslMarker +fun RVM.actionNone( + debounceInterval: Long? = null +): ReadOnlyProperty> = action( + debounceInterval = debounceInterval +) + +@Suppress("unused") +@RvmDslMarker +fun RVM.debouncedAction( + debounceInterval: Long = DEF_ACTION_DEBOUNCE_INTERVAL +): ReadOnlyProperty> = action( + debounceInterval = debounceInterval +) + +@Suppress("unused") +@RvmDslMarker +fun RVM.debouncedActionNone( + debounceInterval: Long = DEF_ACTION_DEBOUNCE_INTERVAL +): ReadOnlyProperty> = action( + debounceInterval = debounceInterval +) diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmSavedStateDelegates.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmSavedStateDelegates.kt index a1e9389..a45d43f 100644 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmSavedStateDelegates.kt +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmSavedStateDelegates.kt @@ -1,166 +1,63 @@ package com.alexdeww.reactiveviewmodel.core import androidx.lifecycle.SavedStateHandle -import com.alexdeww.reactiveviewmodel.core.property.State -import com.alexdeww.reactiveviewmodel.widget.* +import com.alexdeww.reactiveviewmodel.core.annotation.RvmDslMarker +import com.alexdeww.reactiveviewmodel.core.property.RvmState import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty fun SavedStateHandle.delegate( - initValue: (thisRef: ReactiveViewModel, stateHandle: SavedStateHandle, key: String) -> T -): ReadWriteProperty = SavedStateProperty(this, initValue) + initValue: (thisRef: RvmViewModelComponent, stateHandle: SavedStateHandle, key: String) -> T +): ReadWriteProperty = SavedStateProperty(this, initValue) fun SavedStateHandle.value( initialValue: T? = null -): ReadWriteProperty = delegate { _, stateHandle, key -> +): ReadWriteProperty = delegate { _, stateHandle, key -> if (stateHandle.contains(key)) stateHandle[key] else initialValue } fun SavedStateHandle.valueNonNull( defaultValue: T -): ReadWriteProperty = delegate { _, stateHandle, key -> +): ReadWriteProperty = delegate { _, stateHandle, key -> stateHandle[key] ?: defaultValue } +@RvmDslMarker fun SavedStateHandle.state( initialValue: T? = null, debounceInterval: Long? = null -): ReadOnlyProperty> = delegate { thisRef, stateHandle, key -> - val state = State(stateHandle[key] ?: initialValue, debounceInterval) - thisRef.run { state.viewFlowable.subscribe { stateHandle[key] = it }.disposeOnCleared() } +): ReadOnlyProperty> = delegate { thisRef, stateHandle, key -> + val state = RvmState(stateHandle[key] ?: initialValue, debounceInterval) + thisRef.run { state.viewFlowable.subscribe { stateHandle[key] = it }.autoDispose() } state } -fun SavedStateHandle.inputControl( - initialText: String = "", - hideErrorOnUserInput: Boolean = true, - formatter: FormatterAction? = null, - initialEnabled: Boolean = true, - initialVisibility: BaseVisualControl.Visibility = BaseVisualControl.Visibility.VISIBLE -): ReadOnlyProperty = delegate { thisRef, stateHandle, key -> - val textKey = "$key.text" - val errorKey = "$key.error" - val enabledKey = "$key.enabled" - val visibilityKey = "$key.visibility" - val control = InputControl( - initialText = stateHandle[textKey] ?: initialText, - hideErrorOnUserInput = hideErrorOnUserInput, - formatter = formatter, - initialEnabled = stateHandle[enabledKey] ?: initialEnabled, - initialVisibility = stateHandle[visibilityKey] ?: initialVisibility - ) - thisRef.run { - control.value.viewFlowable - .subscribe { stateHandle[textKey] = it } - .disposeOnCleared() - control.error.viewFlowable - .subscribe { stateHandle[errorKey] = it } - .disposeOnCleared() - control.enabled.viewFlowable - .subscribe { stateHandle[enabledKey] = it } - .disposeOnCleared() - control.visibility.viewFlowable - .subscribe { stateHandle[visibilityKey] = it } - .disposeOnCleared() - } - control -} - -fun SavedStateHandle.ratingControl( - initialValue: Float = 0f, - initialEnabled: Boolean = true, - initialVisibility: BaseVisualControl.Visibility = BaseVisualControl.Visibility.VISIBLE -): ReadOnlyProperty = delegate { thisRef, stateHandle, key -> - val ratingKey = "$key.rating" - val enabledKey = "$key.enabled" - val visibilityKey = "$key.visibility" - val control = RatingControl( - initialValue = stateHandle[ratingKey] ?: initialValue, - initialEnabled = stateHandle[enabledKey] ?: initialEnabled, - initialVisibility = stateHandle[visibilityKey] ?: initialVisibility - ) - thisRef.run { - control.value.viewFlowable - .subscribe { stateHandle[ratingKey] = it } - .disposeOnCleared() - control.enabled.viewFlowable - .subscribe { stateHandle[enabledKey] = it } - .disposeOnCleared() - control.visibility.viewFlowable - .subscribe { stateHandle[visibilityKey] = it } - .disposeOnCleared() - } - control -} - -fun SavedStateHandle.displayableControl( - debounceInterval: Long? = null -): ReadOnlyProperty> = - delegate { thisRef, stateHandle, key -> - val actionKey = "$key.action" - val control = DisplayableControl(debounceInterval) - thisRef.run { - control.action.setValue(stateHandle[actionKey] ?: DisplayableControl.Action.Hide) - control.action.viewFlowable - .subscribe { stateHandle[actionKey] = it } - .disposeOnCleared() - } - control - } - -fun SavedStateHandle.checkControl( - initialChecked: Boolean = false, - initialEnabled: Boolean = true, - initialVisibility: BaseVisualControl.Visibility = BaseVisualControl.Visibility.VISIBLE -): ReadOnlyProperty = delegate { thisRef, stateHandle, key -> - val checkedKey = "$key.checked" - val enabledKey = "$key.enabled" - val visibilityKey = "$key.visibility" - val control = CheckControl( - initialChecked = stateHandle[checkedKey] ?: initialChecked, - initialEnabled = stateHandle[enabledKey] ?: initialEnabled, - initialVisibility = stateHandle[visibilityKey] ?: initialVisibility - ) - thisRef.run { - control.value.viewFlowable - .subscribe { stateHandle[checkedKey] = it } - .disposeOnCleared() - control.enabled.viewFlowable - .subscribe { stateHandle[enabledKey] = it } - .disposeOnCleared() - control.visibility.viewFlowable - .subscribe { stateHandle[visibilityKey] = it } - .disposeOnCleared() - } - control -} - @PublishedApi internal class SavedStateProperty( private val savedStateHandle: SavedStateHandle, - private val initValue: (thisRef: ReactiveViewModel, stateHandle: SavedStateHandle, key: String) -> T -) : ReadWriteProperty { + private val initValue: (thisRef: RvmViewModelComponent, stateHandle: SavedStateHandle, key: String) -> T +) : ReadWriteProperty { private object NoneValue private var value: Any? = NoneValue @Suppress("UNCHECKED_CAST") - override fun getValue(thisRef: ReactiveViewModel, property: KProperty<*>): T { + override fun getValue(thisRef: RvmViewModelComponent, property: KProperty<*>): T { if (value === NoneValue) { value = initValue(thisRef, savedStateHandle, getStateKey(thisRef, property)) } return value as T } - override fun setValue(thisRef: ReactiveViewModel, property: KProperty<*>, value: T) { + override fun setValue(thisRef: RvmViewModelComponent, property: KProperty<*>, value: T) { this.value = value savedStateHandle[getStateKey(thisRef, property)] = value } - private fun getStateKey(thisRef: ReactiveViewModel, property: KProperty<*>): String = + private fun getStateKey(thisRef: RvmViewModelComponent, property: KProperty<*>): String = "${thisRef::class.java.simpleName}.${property.name}" } diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmViewComponent.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmViewComponent.kt index f20e74d..d418821 100644 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmViewComponent.kt +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmViewComponent.kt @@ -1,110 +1,33 @@ package com.alexdeww.reactiveviewmodel.core -import android.app.Dialog -import android.widget.CompoundButton -import android.widget.EditText -import android.widget.RatingBar import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import com.alexdeww.reactiveviewmodel.core.property.ConfirmationEvent -import com.alexdeww.reactiveviewmodel.core.property.Event -import com.alexdeww.reactiveviewmodel.core.property.State -import com.alexdeww.reactiveviewmodel.widget.* -import com.google.android.material.textfield.TextInputLayout +import com.alexdeww.reactiveviewmodel.core.RvmAutoDisposableSupport.StoreKey +import com.alexdeww.reactiveviewmodel.core.property.RvmObservableProperty +import com.alexdeww.reactiveviewmodel.widget.RvmBaseControl import io.reactivex.rxjava3.disposables.Disposable -interface RvmViewComponent { +interface RvmViewComponent : RvmAutoDisposableSupport { - val componentLifecycleOwner: LifecycleOwner - - fun Disposable.disposeOnDestroy(tag: String) + companion object { + val onStopStoreKey = StoreKey("RvmViewComponent.onStopStoreKey") + val onDestroyViewStoreKey = StoreKey("RvmViewComponent.onDestroyViewStoreKey") + } - fun Disposable.disposeOnStop(tag: String) + val componentLifecycleOwner: LifecycleOwner - fun Disposable.disposeOnDestroyView(tag: String) + fun Disposable.disposeOnStop(tag: String) = autoDispose(tag, onStopStoreKey) + fun Disposable.disposeOnDestroyView(tag: String) = autoDispose(tag, onDestroyViewStoreKey) + fun Disposable.disposeOnDestroy(tag: String) = autoDispose(tag) fun LiveData.observe(action: OnLiveDataAction): Observer = observe(owner = componentLifecycleOwner, action = action) - fun State.observe(action: OnLiveDataAction): Observer = - observe(componentLifecycleOwner, action) - - fun Event.observe(action: OnLiveDataAction): Observer = - observe(componentLifecycleOwner, action) - - fun ConfirmationEvent.observe(action: OnLiveDataAction): Observer = + fun RvmObservableProperty.observe(action: OnLiveDataAction): Observer = observe(componentLifecycleOwner, action) - fun DisplayableControl.observe( - action: DisplayableAction - ): Observer> = this@observe.action.observe { - action.invoke(it.isShowing, it.getShowingValue()) - } - - fun DisplayableControl.observe( - onShow: (T) -> Unit, - onHide: () -> Unit - ): Observer> = this@observe.action.observe { - when (it) { - is DisplayableControl.Action.Show -> onShow.invoke(it.data) - else -> onHide.invoke() - } - } - - fun CheckControl.bindTo( - compoundButton: CompoundButton, - bindEnable: Boolean = true, - bindVisible: Boolean = true - ) = bindTo( - rvmViewComponent = this@RvmViewComponent, - compoundButton = compoundButton, - bindEnable = bindEnable, - bindVisible = bindVisible - ) - - fun DialogControl.bindTo( - dialogCreator: DialogCreator - ) = bindTo( - rvmViewComponent = this@RvmViewComponent, - dialogCreator = dialogCreator - ) - - fun InputControl.bindTo( - editText: EditText, - bindError: Boolean = false, - bindEnable: Boolean = true, - bindVisible: Boolean = true - ) = bindTo( - rvmViewComponent = this@RvmViewComponent, - editText = editText, - bindError = bindError, - bindEnable = bindEnable, - bindVisible = bindVisible - ) - - fun InputControl.bindTo( - textInputLayout: TextInputLayout, - bindError: Boolean = false, - bindEnable: Boolean = true, - bindVisible: Boolean = true - ) = bindTo( - rvmViewComponent = this@RvmViewComponent, - textInputLayout = textInputLayout, - bindError = bindError, - bindEnable = bindEnable, - bindVisible = bindVisible - ) - - fun RatingControl.bindTo( - ratingBar: RatingBar, - bindEnable: Boolean = true, - bindVisible: Boolean = true - ) = bindTo( - rvmViewComponent = this@RvmViewComponent, - ratingBar = ratingBar, - bindEnable = bindEnable, - bindVisible = bindVisible - ) + val > C.binder: B + get() = getBinder(this@RvmViewComponent) } diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmViewModelComponent.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmViewModelComponent.kt new file mode 100644 index 0000000..3b84660 --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/RvmViewModelComponent.kt @@ -0,0 +1,115 @@ +package com.alexdeww.reactiveviewmodel.core + +import com.alexdeww.reactiveviewmodel.core.annotation.RvmBinderDslMarker +import com.alexdeww.reactiveviewmodel.core.annotation.RvmDslMarker +import com.alexdeww.reactiveviewmodel.core.property.RvmAction +import com.alexdeww.reactiveviewmodel.core.property.RvmProperty +import com.alexdeww.reactiveviewmodel.core.property.RvmState +import com.alexdeww.reactiveviewmodel.core.property.RvmStateProjection +import com.alexdeww.reactiveviewmodel.core.utils.RvmPropertyDelegate +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.subjects.BehaviorSubject +import java.util.concurrent.atomic.AtomicInteger +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +@RvmBinderDslMarker +interface RvmViewModelComponent : RvmPropertiesSupport, RvmAutoDisposableSupport { + + interface Invocable { + val isExecute: Boolean + val isExecuteObservable: Observable + operator fun invoke(params: T) + } + + // Bind Logic + fun Observable.applyDefaultErrorHandler(): Observable = this + + @RvmBinderDslMarker + infix fun RvmAction.bind( + chainBlock: Observable.() -> Observable + ) = bindProperty(this, chainBlock) + + @RvmBinderDslMarker + infix fun RvmState.bind( + chainBlock: Observable.() -> Observable + ) = bindProperty(this, chainBlock) + +} + +@Suppress("unused") +@RvmDslMarker +fun RVM.invocable( + block: (params: T) -> Completable +): ReadOnlyProperty> = + InvocableDelegate(block) + +@Suppress("unused") +@RvmDslMarker +fun RVM.stateProjectionFromSource( + initialValue: T? = null, + sourceBlock: () -> Observable +): ReadOnlyProperty> = RvmPropertyDelegate.def { + val projection = RvmStateProjection(initialValue) + lateSubscription(sourceBlock().doOnNext(projection.consumer)) + projection +} + +private fun RvmViewModelComponent.bindProperty( + rvmProperty: RvmProperty, + chainBlock: Observable.() -> Observable +) { + // 1 - need skip, 2 - has value (skip only if source has value) + val skipState = AtomicInteger(0) + val source = rvmProperty.observable + .replay(1) + .apply { connect().autoDispose() } + .doOnNext { skipState.compareAndSet(0, 2) } + .skipWhile { skipState.compareAndSet(1, 2) } + .chainBlock() + .doOnError { skipState.compareAndSet(2, 1) } + + lateSubscription(source) +} + +private fun RvmViewModelComponent.lateSubscription( + source: Observable<*> +) { + Observable + .defer { source.applyDefaultErrorHandler().retry() } + .subscribeOn(AndroidSchedulers.mainThread()) + .subscribe() + .autoDispose() +} + +private class InvocableDelegate( + private val block: (params: T) -> Completable +) : ReadOnlyProperty> { + + private var value: RvmViewModelComponent.Invocable? = null + + override fun getValue( + thisRef: RvmViewModelComponent, + property: KProperty<*> + ): RvmViewModelComponent.Invocable { + if (value == null) { + val action = RvmAction() + val isExecuteSubj: BehaviorSubject = BehaviorSubject.createDefault(false) + thisRef.run { + action bind { + this.switchMapCompletable { params -> block(params).bindProgress(isExecuteSubj::onNext) } + .toObservable() + } + } + value = object : RvmViewModelComponent.Invocable { + override val isExecute: Boolean get() = isExecuteSubj.value ?: false + override val isExecuteObservable: Observable = isExecuteSubj.serialize() + override fun invoke(params: T) = action.consumer.accept(params) + } + } + return value!! + } + +} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/annotation/RvmBinderDslMarker.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/annotation/RvmBinderDslMarker.kt new file mode 100644 index 0000000..f990462 --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/annotation/RvmBinderDslMarker.kt @@ -0,0 +1,10 @@ +package com.alexdeww.reactiveviewmodel.core.annotation + +@DslMarker +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.TYPEALIAS, + AnnotationTarget.TYPE, + AnnotationTarget.FUNCTION +) +annotation class RvmBinderDslMarker diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/annotation/RvmDslMarker.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/annotation/RvmDslMarker.kt new file mode 100644 index 0000000..6ee4350 --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/annotation/RvmDslMarker.kt @@ -0,0 +1,10 @@ +package com.alexdeww.reactiveviewmodel.core.annotation + +@DslMarker +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.TYPEALIAS, + AnnotationTarget.TYPE, + AnnotationTarget.FUNCTION +) +annotation class RvmDslMarker diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/common/RvmComponent.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/common/RvmComponent.kt deleted file mode 100644 index c3e4706..0000000 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/common/RvmComponent.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.alexdeww.reactiveviewmodel.core.common - -import com.alexdeww.reactiveviewmodel.core.property.Action -import com.alexdeww.reactiveviewmodel.core.property.ConfirmationEvent -import com.alexdeww.reactiveviewmodel.core.property.Event -import com.alexdeww.reactiveviewmodel.core.property.State -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.functions.Consumer - -interface RvmComponent { - - fun State.setValue(value: T) { - this.consumer.accept(value) - } - - fun State.setValueIfChanged(value: T) { - if (this.value != value) this.consumer.accept(value) - } - - val State.consumer: Consumer get() = this.consumer - - val State.observable: Observable get() = this.observable - - val Action.observable: Observable get() = this.observable - - fun Event.call(value: T) = this.consumer.accept(value) - - fun Event.call() = this.consumer.accept(Unit) - - val Event.consumer: Consumer get() = this.consumer - - val Event.observable: Observable get() = this.observable - - fun ConfirmationEvent.call(value: T) = this.consumer.accept(value) - - fun ConfirmationEvent.call() = this.consumer.accept(Unit) - - val ConfirmationEvent.consumer: Consumer get() = this.consumer - - val ConfirmationEvent.observable: Observable get() = this.observable - -} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/Action.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/Action.kt deleted file mode 100644 index 098b0eb..0000000 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/Action.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.alexdeww.reactiveviewmodel.core.property - -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.functions.Consumer -import io.reactivex.rxjava3.subjects.PublishSubject - -class Action internal constructor(debounceInterval: Long? = null) { - - private val subject = PublishSubject.create().toSerialized() - - internal val observable: Observable = subject.letDebounce(debounceInterval) - - val consumer: Consumer = Consumer { subject.onNext(it) } - - fun call(value: T) = consumer.accept(value) - -} \ No newline at end of file diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmAction.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmAction.kt new file mode 100644 index 0000000..f639985 --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmAction.kt @@ -0,0 +1,23 @@ +package com.alexdeww.reactiveviewmodel.core.property + +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.functions.Consumer +import io.reactivex.rxjava3.subjects.PublishSubject + +/** + * В основном предназначен для передачи событий или данных из View во ViewModel. + * + * * Не хранит данные. + */ +class RvmAction internal constructor( + debounceInterval: Long? = null +) : RvmProperty() { + + private val subject = PublishSubject.create().toSerialized() + + override val observable: Observable = subject.letDebounce(debounceInterval) + public override val consumer: Consumer = Consumer { subject.onNext(it) } + + fun call(value: T) = consumer.accept(value) + +} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/ConfirmationEvent.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmConfirmationEvent.kt similarity index 61% rename from reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/ConfirmationEvent.kt rename to reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmConfirmationEvent.kt index 3386f8d..038bc33 100644 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/ConfirmationEvent.kt +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmConfirmationEvent.kt @@ -6,12 +6,25 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.Observer -import com.alexdeww.reactiveviewmodel.core.property.ConfirmationEvent.ObserverWrapper +import com.alexdeww.reactiveviewmodel.core.property.RvmConfirmationEvent.ObserverWrapper import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.functions.Consumer -class ConfirmationEvent internal constructor(debounceInterval: Long? = null) { +/** + * Почти тоже самое, что и [RvmEvent], но отличается тем, что хранит последнее значение + * пока не будет вызван метод [confirm]. + * + * * Передавать данные в [RvmConfirmationEvent] могут только наследники + * [RvmPropertiesSupport][com.alexdeww.reactiveviewmodel.core.RvmPropertiesSupport]. + * + * * Хранит последнее переданное значение пока не будет вызван метод [confirm]. + * Каждый новый подписчик будет получать последнее сохраненное значение, + * пока не вызван метод [confirm]. + */ +class RvmConfirmationEvent internal constructor( + debounceInterval: Long? = null +) : RvmProperty(), RvmCallableProperty { private sealed class EventType { data class Pending(val data: Any) : EventType() @@ -21,28 +34,31 @@ class ConfirmationEvent internal constructor(debounceInterval: Long? = fun tryGetData(): T? = if (this is Pending) data as T else null } - private val eventState = State(EventType.Confirmed, debounceInterval) + private val eventState = RvmState(EventType.Confirmed, debounceInterval) - internal val consumer: Consumer = Consumer { + override val consumer: Consumer = Consumer { eventState.consumer.accept(EventType.Pending(it)) } @Suppress("UNCHECKED_CAST") - internal val observable: Observable = eventState.observable + override val observable: Observable = eventState.observable .ofType(EventType.Pending::class.java) .map { it.data as T } - val liveData: LiveData by lazy { ConfirmationEventLiveData() } - val viewFlowable: Flowable by lazy { observable.toViewFlowable() } + override val liveData: LiveData by lazy { ConfirmationEventLiveData() } + override val viewFlowable: Flowable by lazy { observable.toViewFlowable() } val isConfirmed: Boolean get() = eventState.value === EventType.Confirmed + /** + * Подтверждение, что данные получены + */ fun confirm() { if (!isConfirmed) eventState.consumer.accept(EventType.Confirmed) } private inner class ConfirmationEventLiveData : MediatorLiveData() { - private val observers = ArraySet>() + private val observers = ArraySet>() @MainThread override fun observe(owner: LifecycleOwner, observer: Observer) { diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/Event.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmEvent.kt similarity index 54% rename from reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/Event.kt rename to reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmEvent.kt index 4be67c3..e7bc35d 100644 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/Event.kt +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmEvent.kt @@ -9,18 +9,32 @@ import io.reactivex.rxjava3.functions.Consumer import io.reactivex.rxjava3.subjects.BehaviorSubject import java.util.concurrent.atomic.AtomicBoolean -class Event internal constructor(debounceInterval: Long? = null) { +/** + * В основном предназначен для передачи событий или данных из ViewModel во View. + * + * * Передавать данные в [RvmEvent] могут только наследники + * [RvmPropertiesSupport][com.alexdeww.reactiveviewmodel.core.RvmPropertiesSupport]. + * + * * Хранит последнее переданное значение пока не появится подписчик. + * Только первый подписчик получит последнее сохраненное значение, + * все последующие подписки, будут получать только новые значения. + * + * * Пока есть активная подписка, данные не сохраняются. + */ +class RvmEvent internal constructor( + debounceInterval: Long? = null +) : RvmProperty(), RvmCallableProperty { private val subject = BehaviorSubject.create() private val serializedSubject = subject.toSerialized() private val isPending = AtomicBoolean(false) - internal val consumer: Consumer = Consumer { + override val consumer: Consumer = Consumer { isPending.set(true) serializedSubject.onNext(it) } - internal val observable: Observable = Observable + override val observable: Observable = Observable .create { emitter -> val skipCount = if (!isPending.get() && subject.hasValue()) 1L else 0L val d = serializedSubject.skip(skipCount).subscribe { @@ -32,8 +46,8 @@ class Event internal constructor(debounceInterval: Long? = null) { .letDebounce(debounceInterval) .share() - val liveData: LiveData by lazy { EventLiveData() } - val viewFlowable: Flowable by lazy { observable.toViewFlowable() } + override val liveData: LiveData by lazy { EventLiveData() } + override val viewFlowable: Flowable by lazy { observable.toViewFlowable() } private inner class EventLiveData : LiveEvent() { @@ -51,4 +65,4 @@ class Event internal constructor(debounceInterval: Long? = null) { } -} \ No newline at end of file +} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmProperty.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmProperty.kt new file mode 100644 index 0000000..64ed7b7 --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmProperty.kt @@ -0,0 +1,32 @@ +package com.alexdeww.reactiveviewmodel.core.property + +import androidx.lifecycle.LiveData +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.functions.Consumer + +abstract class RvmPropertyBase { + internal abstract val consumer: Consumer + internal abstract val observable: Observable +} + +@Suppress("UnnecessaryAbstractClass") +abstract class RvmProperty : RvmPropertyBase() + +interface RvmObservableProperty { + val viewFlowable: Flowable + val liveData: LiveData +} + +interface RvmValueProperty : RvmObservableProperty { + val value: T? + val valueNonNull: T get() = value!! + val hasValue: Boolean get() = value != null + + fun getValueOrDef(actionDefValue: () -> T): T = value ?: actionDefValue() + fun getValueOrDef(defValue: T): T = getValueOrDef { defValue } +} + +interface RvmCallableProperty : RvmObservableProperty + +interface RvmMutableValueProperty : RvmValueProperty diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmState.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmState.kt new file mode 100644 index 0000000..c5c48b0 --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmState.kt @@ -0,0 +1,48 @@ +package com.alexdeww.reactiveviewmodel.core.property + +import android.annotation.SuppressLint +import com.alexdeww.reactiveviewmodel.core.livedata.RvmLiveData +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.functions.Consumer +import io.reactivex.rxjava3.subjects.BehaviorSubject + +/** + * В основном предназначен для передачи состояния из ViewModel во View. + * + * * Передавать данные в [RvmState] могут только наследники + * [RvmPropertiesSupport][com.alexdeww.reactiveviewmodel.core.RvmPropertiesSupport]. + * + * * Всегда хранит последнее переданное значение. + * + * * Каждый подписчик в первую очередь получит последннее сохраненное значение. + */ +class RvmState internal constructor( + initialValue: T? = null, + debounceInterval: Long? = null +) : RvmProperty(), RvmMutableValueProperty { + + private val subject = when (initialValue) { + null -> BehaviorSubject.create() + else -> BehaviorSubject.createDefault(initialValue) + } + private val serializedSubject = subject.toSerialized() + + internal var valueChangesHook: ((value: T) -> T)? = null + override val consumer: Consumer = Consumer { newValue -> + serializedSubject.onNext(valueChangesHook?.invoke(newValue) ?: newValue) + } + override val observable: Observable = serializedSubject.letDebounce(debounceInterval) + + override val value: T? get() = subject.value + override val viewFlowable: Flowable by lazy { observable.toViewFlowable() } + override val liveData: RvmLiveData by lazy { StateLiveData() } + + @SuppressLint("CheckResult") + private inner class StateLiveData : RvmLiveData() { + init { + viewFlowable.subscribe { value = it } + } + } + +} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmStateProjection.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmStateProjection.kt new file mode 100644 index 0000000..099347e --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/RvmStateProjection.kt @@ -0,0 +1,43 @@ +package com.alexdeww.reactiveviewmodel.core.property + +import android.annotation.SuppressLint +import com.alexdeww.reactiveviewmodel.core.livedata.RvmLiveData +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.functions.Consumer +import io.reactivex.rxjava3.subjects.BehaviorSubject + +/** + * Почти тоже самое, что и [RvmState], но отличается тем, + * что никто не может передавать данные няпрямую. + * + * * Никто не может передавать данные няпрямую. + * [RvmStateProjection] может получать данные от источников: + * [Observable], [RvmState], [RvmStateProjection], + * либо объекта наследника [RvmPropertyBase] и [RvmValueProperty]. + */ +class RvmStateProjection internal constructor( + initialValue: T? = null, +) : RvmPropertyBase(), RvmValueProperty { + + private val subject = when (initialValue) { + null -> BehaviorSubject.create() + else -> BehaviorSubject.createDefault(initialValue) + } + private val serializedSubject = subject.toSerialized() + + override val consumer: Consumer = Consumer(serializedSubject::onNext) + override val observable: Observable = serializedSubject + + override val value: T? get() = subject.value + override val viewFlowable: Flowable by lazy { observable.toViewFlowable() } + override val liveData: RvmLiveData by lazy { StateLiveData() } + + @SuppressLint("CheckResult") + private inner class StateLiveData : RvmLiveData() { + init { + viewFlowable.subscribe { value = it } + } + } + +} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/State.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/State.kt deleted file mode 100644 index a459b3e..0000000 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/property/State.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.alexdeww.reactiveviewmodel.core.property - -import android.annotation.SuppressLint -import com.alexdeww.reactiveviewmodel.core.livedata.RvmLiveData -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.functions.Consumer -import io.reactivex.rxjava3.subjects.BehaviorSubject - -class State internal constructor( - initValue: T? = null, - debounceInterval: Long? = null -) { - - private val subject = when (initValue) { - null -> BehaviorSubject.create() - else -> BehaviorSubject.createDefault(initValue) - } - private val serializedSubject = subject.toSerialized() - - internal var valueChangesHook: ((value: T) -> T)? = null - internal val consumer: Consumer = Consumer { newValue -> - serializedSubject.onNext(valueChangesHook?.invoke(newValue) ?: newValue) - } - internal val observable: Observable = serializedSubject.letDebounce(debounceInterval) - - val value: T? get() = subject.value - val valueNonNull: T get() = value!! - val hasValue: Boolean get() = value != null - - val liveData: RvmLiveData by lazy { StateLiveData() } - val viewFlowable: Flowable by lazy { observable.toViewFlowable() } - - fun getValueOrDef(actionDefValue: () -> T): T = value ?: actionDefValue() - fun getValueOrDef(defValue: T): T = getValueOrDef { defValue } - - @SuppressLint("CheckResult") - private inner class StateLiveData : RvmLiveData() { - init { - viewFlowable.subscribe { value = it } - } - } - -} \ No newline at end of file diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/utils/RvmPropertyDelegate.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/utils/RvmPropertyDelegate.kt new file mode 100644 index 0000000..1390848 --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/core/utils/RvmPropertyDelegate.kt @@ -0,0 +1,24 @@ +package com.alexdeww.reactiveviewmodel.core.utils + +import com.alexdeww.reactiveviewmodel.core.RvmPropertiesSupport +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +class RvmPropertyDelegate private constructor( + private val initializer: R.(property: KProperty<*>) -> P +) : ReadOnlyProperty { + + companion object { + fun def( + initializer: R.(property: KProperty<*>) -> P + ): ReadOnlyProperty = RvmPropertyDelegate(initializer) + } + + private var value: P? = null + + override fun getValue(thisRef: R, property: KProperty<*>): P { + if (value == null) value = initializer(thisRef, property) + return value!! + } + +} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/BaseControl.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/BaseControl.kt deleted file mode 100644 index 5a4e0a3..0000000 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/BaseControl.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.alexdeww.reactiveviewmodel.widget - -import com.alexdeww.reactiveviewmodel.core.common.RvmComponent -import com.alexdeww.reactiveviewmodel.core.property.Action -import com.alexdeww.reactiveviewmodel.core.property.ConfirmationEvent -import com.alexdeww.reactiveviewmodel.core.property.Event -import com.alexdeww.reactiveviewmodel.core.property.State - -abstract class BaseControl : RvmComponent { - - protected fun state( - initValue: T? = null, - debounceInterval: Long? = null - ): State = State(initValue, debounceInterval) - - protected fun event(debounceInterval: Long? = null): Event = - Event(debounceInterval) - - protected fun eventNone(debounceInterval: Long? = null): Event = - event(debounceInterval) - - protected fun action(debounceInterval: Long? = null): Action = - Action(debounceInterval) - - protected fun actionNone(debounceInterval: Long? = null): Action = - action(debounceInterval) - - protected fun confirmationEvent( - debounceInterval: Long? = null - ): ConfirmationEvent = ConfirmationEvent(debounceInterval) - - protected fun confirmationEventNone( - debounceInterval: Long? = null - ): ConfirmationEvent = confirmationEvent(debounceInterval) - -} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/BaseVisualControl.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/BaseVisualControl.kt deleted file mode 100644 index 469a25e..0000000 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/BaseVisualControl.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.alexdeww.reactiveviewmodel.widget - -import android.view.View -import androidx.annotation.CallSuper -import androidx.lifecycle.MediatorLiveData -import com.alexdeww.reactiveviewmodel.core.RvmViewComponent -import io.reactivex.rxjava3.functions.Consumer -import java.lang.ref.WeakReference - -abstract class BaseVisualControl( - initialValue: T, - initialEnabled: Boolean, - initialVisibility: Visibility -) : BaseControl() { - - enum class Visibility(val value: Int) { - VISIBLE(View.VISIBLE), - INVISIBLE(View.INVISIBLE), - GONE(View.GONE) - } - - val value = state(initialValue) - val enabled = state(initialEnabled) - val visibility = state(initialVisibility) - - val actionChangeValue = action() - - init { - actionChangeValue.observable - .filter { it != value.value } - .subscribe(::onChangedValue) - } - - @CallSuper - protected open fun onChangedValue(newValue: T) { - value.consumer.accept(newValue) - } - -} - -typealias ActionOnValueChanged = (newValue: T) -> Unit -typealias ActionOnActive = VisualControlLiveDataMediator.() -> Unit -typealias ActionOnInactive = VisualControlLiveDataMediator.() -> Unit - -fun , T : Any, V : View> C.baseBindTo( - rvmViewComponent: RvmViewComponent, - view: V, - bindEnable: Boolean, - bindVisible: Boolean, - onValueChanged: ActionOnValueChanged, - onActiveAction: ActionOnActive, - onInactiveAction: ActionOnInactive -) { - val liveData = VisualControlLiveDataMediator( - control = this@baseBindTo, - view = view, - bindEnable = bindEnable, - bindVisible = bindVisible, - onValueChanged = onValueChanged, - onActiveAction = onActiveAction, - onInactiveAction = onInactiveAction - ) - rvmViewComponent.run { liveData.observe { /* empty */ } } -} - -class VisualControlLiveDataMediator( - control: BaseVisualControl, - view: View, - private val bindEnable: Boolean, - private val bindVisible: Boolean, - private val onValueChanged: ActionOnValueChanged, - private val onActiveAction: ActionOnActive, - private val onInactiveAction: ActionOnInactive -) : MediatorLiveData() { - - private val viewRef = WeakReference(view) - private val view: View? get() = viewRef.get() - private val controlRef = WeakReference(control) - private val control: BaseVisualControl? get() = controlRef.get() - private var isEditing: Boolean = false - - val changeValueConsumer = Consumer { - if (!isEditing) this.control?.actionChangeValue?.call(it) - } - - override fun onActive() { - super.onActive() - control?.apply { - if (bindEnable) addSource(enabled.liveData) { view?.isEnabled = it } - if (bindVisible) addSource(visibility.liveData) { view?.visibility = it.value } - addSource(value.liveData) { newValue -> - isEditing = true - onValueChanged(newValue) - isEditing = false - } - } - onActiveAction.invoke(this) - } - - override fun onInactive() { - control?.apply { - if (bindEnable) removeSource(enabled.liveData) - if (bindVisible) removeSource(visibility.liveData) - removeSource(value.liveData) - } - onInactiveAction.invoke(this) - super.onInactive() - } - -} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/CheckControl.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/CheckControl.kt deleted file mode 100644 index cf89f44..0000000 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/CheckControl.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.alexdeww.reactiveviewmodel.widget - -import android.annotation.SuppressLint -import android.widget.CompoundButton -import com.alexdeww.reactiveviewmodel.core.RvmViewComponent - -@SuppressLint("CheckResult") -class CheckControl internal constructor( - initialChecked: Boolean, - initialEnabled: Boolean, - initialVisibility: Visibility -) : BaseVisualControl(initialChecked, initialEnabled, initialVisibility) - -fun checkControl( - initialChecked: Boolean = false, - initialEnabled: Boolean = true, - initialVisibility: BaseVisualControl.Visibility = BaseVisualControl.Visibility.VISIBLE -): CheckControl = CheckControl( - initialChecked = initialChecked, - initialEnabled = initialEnabled, - initialVisibility = initialVisibility -) - -fun CheckControl.bindTo( - rvmViewComponent: RvmViewComponent, - compoundButton: CompoundButton, - bindEnable: Boolean = true, - bindVisible: Boolean = true -) = baseBindTo( - rvmViewComponent = rvmViewComponent, - view = compoundButton, - bindEnable = bindEnable, - bindVisible = bindVisible, - onValueChanged = { compoundButton.isChecked = it }, - onActiveAction = { - compoundButton.setOnCheckedChangeListener { _, isChecked -> - changeValueConsumer.accept(isChecked) - } - }, - onInactiveAction = { compoundButton.setOnCheckedChangeListener(null) } -) diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/DialogControl.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/DialogControl.kt deleted file mode 100644 index e1762ce..0000000 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/DialogControl.kt +++ /dev/null @@ -1,159 +0,0 @@ -package com.alexdeww.reactiveviewmodel.widget - -import android.app.Dialog -import androidx.lifecycle.MediatorLiveData -import com.alexdeww.reactiveviewmodel.core.RvmViewComponent -import io.reactivex.rxjava3.core.Maybe - -sealed class DialogResult { - object Accept : DialogResult() - object Cancel : DialogResult() -} - -class DialogControl internal constructor() : BaseControl() { - - sealed class Display { - data class Displayed(val data: T) : Display() - object Absent : Display() - } - - internal val result = action() - - val displayed = state>(Display.Absent) - val isShowing get() = displayed.value is Display.Displayed - - fun show(data: T) { - dismiss() - displayed.consumer.accept(Display.Displayed(data)) - } - - fun showForResult(data: T, dismissOnDispose: Boolean = false): Maybe { - dismiss() - return result - .observable - .doOnSubscribe { displayed.consumer.accept(Display.Displayed(data)) } - .doOnDispose { if (dismissOnDispose) dismiss() } - .takeUntil( - displayed.observable - .skip(1) - .filter { it == Display.Absent } - ) - .firstElement() - } - - fun dismiss() { - if (isShowing) displayed.consumer.accept(Display.Absent) - } - -} - -class DialogControlResult internal constructor( - private val dialogControl: DialogControl<*, R> -) { - - fun sendResult(result: R) { - dialogControl.result.consumer.accept(result) - dialogControl.dismiss() - } - - fun sendResultWithoutDismiss(result: R) { - dialogControl.result.consumer.accept(result) - } - - fun dismiss() { - dialogControl.dismiss() - } - -} - -fun dialogControl(): DialogControl = DialogControl() - -fun dialogControlWithResult(): DialogControl = DialogControl() - -typealias DialogCreator = (data: T, dc: DialogControlResult) -> D - -interface DialogHandlerListener { - - fun onSetupOnDismiss(dialog: D, dismissAction: () -> Unit) - - fun onShowDialog(dialog: D) - - fun onCloseDialog(dialog: D) - - fun onDialogUnbind(dialog: D) { - onCloseDialog(dialog) - } - -} - -fun DialogControl.bindToEx( - rvmViewComponent: RvmViewComponent, - dialogCreator: DialogCreator, - dialogHandlerListener: DialogHandlerListener -) { - val liveData = DialogLiveDataMediator(this, dialogCreator, dialogHandlerListener) - rvmViewComponent.run { liveData.observe { /* empty */ } } -} - -fun DialogControl.bindTo( - rvmViewComponent: RvmViewComponent, - dialogCreator: DialogCreator -) = bindToEx(rvmViewComponent, dialogCreator, OrdinaryDialogHandlerListener()) - -private class DialogLiveDataMediator( - private val control: DialogControl, - private val dialogCreator: DialogCreator, - private val dialogHandlerListener: DialogHandlerListener -) : MediatorLiveData>(), - DialogHandlerListener by dialogHandlerListener { - - private var dialog: D? = null - - override fun onActive() { - super.onActive() - addSource(control.displayed.liveData) { displayData -> - value = displayData - when (displayData) { - is DialogControl.Display.Displayed -> { - dialog = dialogCreator(displayData.data, DialogControlResult(control)).also { - onSetupOnDismiss(it) { control.dismiss() } - onShowDialog(it) - } - } - DialogControl.Display.Absent -> { - dialog?.let { onCloseDialog(it) } - releaseDialog() - } - } - } - } - - override fun onInactive() { - super.onInactive() - removeSource(control.displayed.liveData) - dialog?.let { onDialogUnbind(it) } - releaseDialog() - } - - private fun releaseDialog() { - dialog = null - } - -} - -private class OrdinaryDialogHandlerListener : DialogHandlerListener { - - override fun onSetupOnDismiss(dialog: Dialog, dismissAction: () -> Unit) { - dialog.setOnDismissListener { dismissAction() } - } - - override fun onShowDialog(dialog: Dialog) { - dialog.show() - } - - override fun onCloseDialog(dialog: Dialog) { - dialog.setOnDismissListener(null) - dialog.dismiss() - } - -} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/DisplayableControl.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/DisplayableControl.kt deleted file mode 100644 index 323c508..0000000 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/DisplayableControl.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.alexdeww.reactiveviewmodel.widget - -import android.os.Parcelable -import androidx.lifecycle.Observer -import com.alexdeww.reactiveviewmodel.core.RvmViewComponent -import kotlinx.parcelize.Parcelize -import kotlinx.parcelize.RawValue - -class DisplayableControl internal constructor( - debounceInterval: Long? = null -) : BaseControl() { - - sealed class Action : Parcelable { - - @Parcelize - object Hide : Action() - - @Parcelize - data class Show(val data: @RawValue T) : Action() - - val isShowing: Boolean get() = this is Show - fun getShowingValue(): T? = (this as? Show)?.data - } - - val action = state>(Action.Hide, debounceInterval) - val isShowing get() = action.value?.isShowing ?: false - val showingValue: T? get() = action.value?.getShowingValue() - - fun show(data: T) { - action.consumer.accept(Action.Show(data)) - } - - fun hide() { - action.consumer.accept(Action.Hide) - } - -} - -fun displayableControl(debounceInterval: Long? = null): DisplayableControl = - DisplayableControl(debounceInterval) - -typealias DisplayableAction = (isVisible: Boolean, data: T?) -> Unit - -fun DisplayableControl.observe( - rvmViewComponent: RvmViewComponent, - action: DisplayableAction -): Observer> = rvmViewComponent.run { - this@observe.action.observe { action.invoke(it.isShowing, it.getShowingValue()) } -} - -fun DisplayableControl.observe( - rvmViewComponent: RvmViewComponent, - onShow: (T) -> Unit, - onHide: () -> Unit -): Observer> = rvmViewComponent.run { - this@observe.action.observe { - when (it) { - is DisplayableControl.Action.Show -> onShow.invoke(it.data) - else -> onHide.invoke() - } - } -} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/InputControl.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/InputControl.kt deleted file mode 100644 index 8b2b636..0000000 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/InputControl.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.alexdeww.reactiveviewmodel.widget - -import android.text.* -import android.view.View -import android.widget.EditText -import com.alexdeww.reactiveviewmodel.core.RvmViewComponent -import com.google.android.material.textfield.TextInputLayout - -typealias FormatterAction = (text: String) -> String - -class InputControl internal constructor( - initialText: String, - private val hideErrorOnUserInput: Boolean, - formatter: FormatterAction?, - initialEnabled: Boolean, - initialVisibility: Visibility -) : BaseVisualControl(initialText, initialEnabled, initialVisibility) { - - init { - value.valueChangesHook = formatter - } - - val error = state() - - override fun onChangedValue(newValue: String) { - if (hideErrorOnUserInput) error.consumer.accept("") - super.onChangedValue(newValue) - } - -} - -fun inputControl( - initialText: String = "", - hideErrorOnUserInput: Boolean = true, - formatter: FormatterAction? = null, - initialEnabled: Boolean = true, - initialVisibility: BaseVisualControl.Visibility = BaseVisualControl.Visibility.VISIBLE -): InputControl = InputControl( - initialText = initialText, - hideErrorOnUserInput = hideErrorOnUserInput, - formatter = formatter, - initialEnabled = initialEnabled, - initialVisibility = initialVisibility -) - -fun InputControl.bindTo( - rvmViewComponent: RvmViewComponent, - editText: EditText, - bindError: Boolean = false, - bindEnable: Boolean = true, - bindVisible: Boolean = true -) = bindTo( - rvmViewComponent = rvmViewComponent, - view = editText, - editText = editText, - actionOnError = { editText.error = it }, - bindError = bindError, - bindEnable = bindEnable, - bindVisible = bindVisible -) - -fun InputControl.bindTo( - rvmViewComponent: RvmViewComponent, - textInputLayout: TextInputLayout, - bindError: Boolean = false, - bindEnable: Boolean = true, - bindVisible: Boolean = true -) = bindTo( - rvmViewComponent = rvmViewComponent, - view = textInputLayout, - editText = textInputLayout.editText!!, - actionOnError = { textInputLayout.error = it }, - bindError = bindError, - bindEnable = bindEnable, - bindVisible = bindVisible -) - -internal fun InputControl.bindTo( - rvmViewComponent: RvmViewComponent, - view: View, - editText: EditText, - actionOnError: (String) -> Unit, - bindError: Boolean = false, - bindEnable: Boolean = true, - bindVisible: Boolean = true -) { - var textWatcher: TextWatcher? = null - baseBindTo( - rvmViewComponent = rvmViewComponent, - view = view, - bindEnable = bindEnable, - bindVisible = bindVisible, - onValueChanged = { newValue -> - val editable = editText.text - if (!newValue.contentEquals(editable)) { - if (editable is Spanned) { - val ss = SpannableString(newValue) - TextUtils.copySpansFrom(editable, 0, ss.length, null, ss, 0) - editable.replace(0, editable.length, ss) - } else { - editable.replace(0, editable.length, newValue) - } - } - }, - onActiveAction = { - if (bindError) addSource(error.liveData) { actionOnError.invoke(it) } - textWatcher = onTextChangedWatcher { changeValueConsumer.accept(it.toString()) } - editText.addTextChangedListener(textWatcher) - }, - onInactiveAction = { - if (bindError) removeSource(error.liveData) - textWatcher?.let { editText.removeTextChangedListener(it) } - textWatcher = null - } - ) -} - -private fun onTextChangedWatcher( - action: (CharSequence) -> Unit -): TextWatcher = object : TextWatcher { - - override fun afterTextChanged(s: Editable?) { - /* nothing */ - } - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { - /* nothing */ - } - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - if (s != null) action.invoke(s) - } - -} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RatingControl.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RatingControl.kt deleted file mode 100644 index 3a3ddb1..0000000 --- a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RatingControl.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.alexdeww.reactiveviewmodel.widget - -import android.annotation.SuppressLint -import android.widget.RatingBar -import com.alexdeww.reactiveviewmodel.core.RvmViewComponent - -@SuppressLint("CheckResult") -class RatingControl internal constructor( - initialValue: Float, - initialEnabled: Boolean, - initialVisibility: Visibility -) : BaseVisualControl(initialValue, initialEnabled, initialVisibility) - -fun ratingControl( - initialValue: Float = 0f, - initialEnabled: Boolean = true, - initialVisibility: BaseVisualControl.Visibility = BaseVisualControl.Visibility.VISIBLE -): RatingControl = RatingControl( - initialValue = initialValue, - initialEnabled = initialEnabled, - initialVisibility = initialVisibility -) - -fun RatingControl.bindTo( - rvmViewComponent: RvmViewComponent, - ratingBar: RatingBar, - bindEnable: Boolean = true, - bindVisible: Boolean = true -) = baseBindTo( - rvmViewComponent = rvmViewComponent, - view = ratingBar, - bindEnable = bindEnable, - bindVisible = bindVisible, - onValueChanged = { ratingBar.rating = it }, - onActiveAction = { - ratingBar.setOnRatingBarChangeListener { _, rating, _ -> changeValueConsumer.accept(rating) } - }, - onInactiveAction = { ratingBar.onRatingBarChangeListener = null } -) diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmBaseControl.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmBaseControl.kt new file mode 100644 index 0000000..ba59f2a --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmBaseControl.kt @@ -0,0 +1,50 @@ +package com.alexdeww.reactiveviewmodel.widget + +import com.alexdeww.reactiveviewmodel.core.RvmPropertiesSupport +import com.alexdeww.reactiveviewmodel.core.RvmViewComponent +import com.alexdeww.reactiveviewmodel.core.property.RvmCallableProperty +import com.alexdeww.reactiveviewmodel.core.property.RvmMutableValueProperty +import com.alexdeww.reactiveviewmodel.core.property.RvmProperty +import com.alexdeww.reactiveviewmodel.core.property.RvmPropertyBase +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.functions.Consumer +import java.lang.ref.WeakReference + +@Suppress("UnnecessaryAbstractClass") +abstract class RvmBaseControl : RvmPropertiesSupport { + + private val defaultPropertiesSupport = object : RvmPropertiesSupport {} + + abstract class ViewBinder(rvmViewComponent: RvmViewComponent) { + protected val rvmViewComponentRef: WeakReference = + WeakReference(rvmViewComponent) + } + + internal abstract fun getBinder(rvmViewComponent: RvmViewComponent): B + + final override val RvmProperty.consumer: Consumer + get() = defaultPropertiesSupport.run { consumer } + final override val RvmPropertyBase.observable: Observable + get() = defaultPropertiesSupport.run { observable } + + final override fun R.call(value: T) where R : RvmCallableProperty, + R : RvmProperty { + defaultPropertiesSupport.run { call(value) } + } + + final override fun R.call() where R : RvmCallableProperty, + R : RvmProperty { + defaultPropertiesSupport.run { call() } + } + + final override fun R.setValue(value: T) where R : RvmMutableValueProperty, + R : RvmProperty { + defaultPropertiesSupport.run { setValue(value) } + } + + final override fun R.setValueIfChanged(value: T) where R : RvmMutableValueProperty, + R : RvmProperty { + defaultPropertiesSupport.run { setValueIfChanged(value) } + } + +} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmBaseVisualControl.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmBaseVisualControl.kt new file mode 100644 index 0000000..0f61b4c --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmBaseVisualControl.kt @@ -0,0 +1,172 @@ +package com.alexdeww.reactiveviewmodel.widget + +import android.view.View +import androidx.annotation.CallSuper +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.SavedStateHandle +import com.alexdeww.reactiveviewmodel.core.* +import com.alexdeww.reactiveviewmodel.widget.RvmBaseVisualControl.Visibility +import io.reactivex.rxjava3.functions.Consumer +import java.lang.ref.WeakReference +import kotlin.properties.ReadOnlyProperty + +typealias RvmActionOnValueChanged = (newValue: T) -> Unit +typealias RvmActionOnActive = RvmVisualControlLiveDataMediator.() -> Unit +typealias RvmActionOnInactive = RvmVisualControlLiveDataMediator.() -> Unit + +abstract class RvmBaseVisualControl>( + initialValue: T, + initialEnabled: Boolean, + initialVisibility: Visibility +) : RvmBaseControl() { + + abstract class BaseBinder( + rvmViewComponent: RvmViewComponent + ) : ViewBinder(rvmViewComponent) { + + protected abstract val control: RvmBaseVisualControl + + @Suppress("LongParameterList") + protected fun bindTo( + view: V, + bindEnable: Boolean, + bindVisible: Boolean, + onValueChanged: RvmActionOnValueChanged, + onActiveAction: RvmActionOnActive, + onInactiveAction: RvmActionOnInactive + ) { + val liveData = RvmVisualControlLiveDataMediator( + control = control, + view = view, + bindEnable = bindEnable, + bindVisible = bindVisible, + onValueChanged = onValueChanged, + onActiveAction = onActiveAction, + onInactiveAction = onInactiveAction + ) + rvmViewComponentRef.get()?.run { liveData.observe { /* empty */ } } + } + + } + + enum class Visibility(val value: Int) { + VISIBLE(View.VISIBLE), + INVISIBLE(View.INVISIBLE), + GONE(View.GONE) + } + + protected val dataInternal by RVM.state(initialValue) + internal val dataInternalAccess = dataInternal + + val data by RVM.stateProjection(dataInternal, false) { it } + val enabled by RVM.state(initialEnabled) + val visibility by RVM.state(initialVisibility) + + val actionChangeDataValue by RVM.action() + + init { + actionChangeDataValue.observable + .filter { it != dataInternal.value } + .subscribe(::onDataValueChanged) + } + + @CallSuper + protected open fun onDataValueChanged(newValue: T) { + dataInternal.consumer.accept(newValue) + } + +} + +@Suppress("LongParameterList") +class RvmVisualControlLiveDataMediator internal constructor( + control: RvmBaseVisualControl, + view: View, + private val bindEnable: Boolean, + private val bindVisible: Boolean, + private val onValueChanged: RvmActionOnValueChanged, + private val onActiveAction: RvmActionOnActive, + private val onInactiveAction: RvmActionOnInactive +) : MediatorLiveData() { + + private val viewRef = WeakReference(view) + private val view: View? get() = viewRef.get() + private val controlRef = WeakReference(control) + private val control: RvmBaseVisualControl? get() = controlRef.get() + private var isEditing: Boolean = false + + val changeValueConsumer = Consumer { + if (!isEditing) this.control?.actionChangeDataValue?.call(it) + } + + override fun onActive() { + super.onActive() + control?.apply { + if (bindEnable) addSource(enabled.liveData) { view?.isEnabled = it } + if (bindVisible) addSource(visibility.liveData) { view?.visibility = it.value } + addSource(dataInternalAccess.liveData) { newValue -> + isEditing = true + onValueChanged(newValue) + isEditing = false + } + } + onActiveAction.invoke(this) + } + + override fun onInactive() { + control?.apply { + if (bindEnable) removeSource(enabled.liveData) + if (bindVisible) removeSource(visibility.liveData) + removeSource(dataInternalAccess.liveData) + } + onInactiveAction.invoke(this) + super.onInactive() + } + +} + +typealias RvmInitControl = ( + value: T, + isEnabled: Boolean, + visibility: Visibility, + stateHandle: SavedStateHandle, + key: String, +) -> C + +fun > SavedStateHandle.visualControlDelegate( + initialValue: T, + initialEnabled: Boolean, + initialVisibility: Visibility, + initControl: RvmInitControl, + watcher: RvmViewModelComponent.(stateHandle: SavedStateHandle, key: String) -> Unit = { _, _ -> } +): ReadOnlyProperty = delegate { thisRef, stateHandle, key -> + val dataKey = "$key.data" + val enabledKey = "$key.enabled" + val visibilityKey = "$key.visibility" + val control = initControl( + stateHandle[dataKey] ?: initialValue, + stateHandle[enabledKey] ?: initialEnabled, + stateHandle[visibilityKey] ?: initialVisibility, + stateHandle, key + ) + thisRef.run { + control.data.viewFlowable + .subscribe { stateHandle[dataKey] = it } + .autoDispose() + control.enabled.viewFlowable + .subscribe { stateHandle[enabledKey] = it } + .autoDispose() + control.visibility.viewFlowable + .subscribe { stateHandle[visibilityKey] = it } + .autoDispose() + watcher(stateHandle, key) + } + control +} + +typealias RvmControlDefaultConstructor = (value: T, isEnabled: Boolean, visibility: Visibility) -> C + +inline fun > rvmControlDefaultConstructor( + crossinline defaultConstructor: RvmControlDefaultConstructor +): RvmInitControl = { value: T, isEnabled: Boolean, visibility: Visibility, _, _ -> + defaultConstructor(value, isEnabled, visibility) +} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmCheckControl.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmCheckControl.kt new file mode 100644 index 0000000..bd3583c --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmCheckControl.kt @@ -0,0 +1,77 @@ +package com.alexdeww.reactiveviewmodel.widget + +import android.widget.CompoundButton +import androidx.lifecycle.SavedStateHandle +import com.alexdeww.reactiveviewmodel.core.* +import com.alexdeww.reactiveviewmodel.core.annotation.RvmBinderDslMarker +import com.alexdeww.reactiveviewmodel.core.annotation.RvmDslMarker +import com.alexdeww.reactiveviewmodel.core.utils.RvmPropertyDelegate +import kotlin.properties.ReadOnlyProperty + +class RvmCheckControl internal constructor( + initialChecked: Boolean, + initialEnabled: Boolean, + initialVisibility: Visibility +) : RvmBaseVisualControl( + initialValue = initialChecked, + initialEnabled = initialEnabled, + initialVisibility = initialVisibility +) { + + override fun getBinder(rvmViewComponent: RvmViewComponent): Binder = Binder(rvmViewComponent) + + inner class Binder internal constructor( + rvmViewComponent: RvmViewComponent + ) : BaseBinder(rvmViewComponent) { + + override val control: RvmBaseVisualControl get() = this@RvmCheckControl + + @RvmBinderDslMarker + fun bindTo( + compoundButton: CompoundButton, + bindEnable: Boolean = true, + bindVisible: Boolean = true + ) { + bindTo( + view = compoundButton, + bindEnable = bindEnable, + bindVisible = bindVisible, + onValueChanged = { compoundButton.isChecked = it }, + onActiveAction = { + compoundButton.setOnCheckedChangeListener { _, isChecked -> + changeValueConsumer.accept(isChecked) + } + }, + onInactiveAction = { compoundButton.setOnCheckedChangeListener(null) } + ) + } + + } + +} + +@Suppress("unused") +@RvmDslMarker +fun RVM.checkControl( + initialChecked: Boolean = false, + initialEnabled: Boolean = true, + initialVisibility: RvmBaseVisualControl.Visibility = RvmBaseVisualControl.Visibility.VISIBLE +): ReadOnlyProperty = RvmPropertyDelegate.def { + RvmCheckControl( + initialChecked = initialChecked, + initialEnabled = initialEnabled, + initialVisibility = initialVisibility + ) +} + +@RvmDslMarker +fun SavedStateHandle.checkControl( + initialChecked: Boolean = false, + initialEnabled: Boolean = true, + initialVisibility: RvmBaseVisualControl.Visibility = RvmBaseVisualControl.Visibility.VISIBLE +): ReadOnlyProperty = visualControlDelegate( + initialValue = initialChecked, + initialEnabled = initialEnabled, + initialVisibility = initialVisibility, + initControl = rvmControlDefaultConstructor(::RvmCheckControl) +) diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmDialogControl.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmDialogControl.kt new file mode 100644 index 0000000..ee0ab1e --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmDialogControl.kt @@ -0,0 +1,186 @@ +package com.alexdeww.reactiveviewmodel.widget + +import android.app.Dialog +import androidx.lifecycle.MediatorLiveData +import com.alexdeww.reactiveviewmodel.core.* +import com.alexdeww.reactiveviewmodel.core.annotation.RvmBinderDslMarker +import com.alexdeww.reactiveviewmodel.core.annotation.RvmDslMarker +import com.alexdeww.reactiveviewmodel.core.utils.RvmPropertyDelegate +import io.reactivex.rxjava3.core.Maybe +import kotlin.properties.ReadOnlyProperty + +typealias RvmDialogCreator = (data: T, dc: RvmDialogControlResult) -> D + +sealed class RvmDefaultDialogResult { + object Accept : RvmDefaultDialogResult() + object Cancel : RvmDefaultDialogResult() +} + +class RvmDialogControl internal constructor() : + RvmBaseControl.Binder>() { + + sealed class Display { + data class Displayed(val data: T) : Display() + object Absent : Display() + } + + internal val result by RVM.action() + internal val displayedInternal by RVM.state>(Display.Absent) + + val displayed by RVM.stateProjection(displayedInternal, false) { it } + val isShowing get() = displayed.value is Display.Displayed + + fun show(data: T) { + dismiss() + displayedInternal.consumer.accept(Display.Displayed(data)) + } + + fun showForResult(data: T, dismissOnDispose: Boolean = false): Maybe { + dismiss() + return result + .observable + .doOnSubscribe { displayedInternal.consumer.accept(Display.Displayed(data)) } + .doOnDispose { if (dismissOnDispose) dismiss() } + .takeUntil( + displayedInternal.observable + .skip(1) + .filter { it == Display.Absent } + ) + .firstElement() + } + + fun dismiss() { + if (isShowing) displayedInternal.consumer.accept(Display.Absent) + } + + override fun getBinder(rvmViewComponent: RvmViewComponent): Binder = Binder(rvmViewComponent) + + inner class Binder internal constructor( + rvmViewComponent: RvmViewComponent + ) : ViewBinder(rvmViewComponent) { + + @RvmBinderDslMarker + fun bindTo( + dialogHandlerListener: RvmDialogHandlerListener, + dialogCreator: RvmDialogCreator, + ) { + val liveData = RvmDialogLiveDataMediator( + control = this@RvmDialogControl, + dialogCreator = dialogCreator, + dialogHandlerListener = dialogHandlerListener + ) + rvmViewComponentRef.get()?.run { liveData.observe { /* empty */ } } + } + + @RvmBinderDslMarker + fun bindTo(dialogCreator: RvmDialogCreator) = bindTo( + dialogHandlerListener = RvmOrdinaryDialogHandlerListener(), + dialogCreator = dialogCreator + ) + + } + +} + +class RvmDialogControlResult internal constructor( + private val dialogControl: RvmDialogControl<*, R> +) { + + fun sendResult(result: R) { + dialogControl.result.consumer.accept(result) + dialogControl.dismiss() + } + + fun sendResultWithoutDismiss(result: R) { + dialogControl.result.consumer.accept(result) + } + + fun dismiss() { + dialogControl.dismiss() + } + +} + +@Suppress("unused") +@RvmDslMarker +fun RVM.dialogControl(): ReadOnlyProperty< + RvmPropertiesSupport, + RvmDialogControl> = RvmPropertyDelegate.def { RvmDialogControl() } + +@Suppress("unused") +@RvmDslMarker +fun RVM.dialogControlDefaultResult(): ReadOnlyProperty< + RvmPropertiesSupport, + RvmDialogControl> = dialogControl() + +interface RvmDialogHandlerListener { + + fun onSetupOnDismiss(dialog: D, dismissAction: () -> Unit) + + fun onShowDialog(dialog: D) + + fun onCloseDialog(dialog: D) + + fun onDialogUnbind(dialog: D) { + onCloseDialog(dialog) + } + +} + +private class RvmDialogLiveDataMediator( + private val control: RvmDialogControl, + private val dialogCreator: RvmDialogCreator, + private val dialogHandlerListener: RvmDialogHandlerListener +) : MediatorLiveData>(), + RvmDialogHandlerListener by dialogHandlerListener { + + private var dialog: D? = null + + override fun onActive() { + super.onActive() + addSource(control.displayedInternal.liveData) { displayData -> + value = displayData + when (displayData) { + is RvmDialogControl.Display.Displayed -> { + dialog = dialogCreator(displayData.data, RvmDialogControlResult(control)).also { + onSetupOnDismiss(it) { control.dismiss() } + onShowDialog(it) + } + } + RvmDialogControl.Display.Absent -> { + dialog?.let { onCloseDialog(it) } + releaseDialog() + } + } + } + } + + override fun onInactive() { + super.onInactive() + removeSource(control.displayedInternal.liveData) + dialog?.let { onDialogUnbind(it) } + releaseDialog() + } + + private fun releaseDialog() { + dialog = null + } + +} + +private class RvmOrdinaryDialogHandlerListener : RvmDialogHandlerListener { + + override fun onSetupOnDismiss(dialog: Dialog, dismissAction: () -> Unit) { + dialog.setOnDismissListener { dismissAction() } + } + + override fun onShowDialog(dialog: Dialog) { + dialog.show() + } + + override fun onCloseDialog(dialog: Dialog) { + dialog.setOnDismissListener(null) + dialog.dismiss() + } + +} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmDisplayableControl.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmDisplayableControl.kt new file mode 100644 index 0000000..8a7aaa6 --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmDisplayableControl.kt @@ -0,0 +1,99 @@ +package com.alexdeww.reactiveviewmodel.widget + +import android.os.Parcelable +import androidx.lifecycle.SavedStateHandle +import com.alexdeww.reactiveviewmodel.core.* +import com.alexdeww.reactiveviewmodel.core.annotation.RvmBinderDslMarker +import com.alexdeww.reactiveviewmodel.core.annotation.RvmDslMarker +import com.alexdeww.reactiveviewmodel.core.utils.RvmPropertyDelegate +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue +import kotlin.properties.ReadOnlyProperty + +typealias RvmDisplayableAction = (isVisible: Boolean, data: T?) -> Unit + +class RvmDisplayableControl internal constructor( + debounceInterval: Long? = null +) : RvmBaseControl.Binder>() { + + sealed class Action : Parcelable { + + @Parcelize + object Hide : Action() + + @Parcelize + data class Show(val data: @RawValue T) : Action() + + val isShowing: Boolean get() = this is Show + fun getShowingValue(): T? = (this as? Show)?.data + + } + + val action by RVM.state>(Action.Hide, debounceInterval) + val isShowing get() = action.value?.isShowing ?: false + val showingValue: T? get() = action.value?.getShowingValue() + + fun show(data: T) { + action.consumer.accept(Action.Show(data)) + } + + fun hide() { + action.consumer.accept(Action.Hide) + } + + override fun getBinder(rvmViewComponent: RvmViewComponent): Binder = Binder(rvmViewComponent) + + inner class Binder internal constructor( + rvmViewComponent: RvmViewComponent + ) : ViewBinder(rvmViewComponent) { + + @RvmBinderDslMarker + fun bind(action: RvmDisplayableAction) { + rvmViewComponentRef.get()?.run { + this@RvmDisplayableControl.action.observe { + action.invoke(it.isShowing, it.getShowingValue()) + } + } + } + + @RvmBinderDslMarker + fun bind( + onShow: (T) -> Unit, + onHide: () -> Unit + ) { + rvmViewComponentRef.get()?.run { + this@RvmDisplayableControl.action.observe { + when (it) { + is Action.Show -> onShow.invoke(it.data) + else -> onHide.invoke() + } + } + } + } + + } + +} + +@Suppress("unused") +@RvmDslMarker +fun RVM.displayableControl( + debounceInterval: Long? = null +): ReadOnlyProperty> = RvmPropertyDelegate.def { + RvmDisplayableControl(debounceInterval) +} + +@RvmDslMarker +fun SavedStateHandle.displayableControl( + debounceInterval: Long? = null +): ReadOnlyProperty> = delegate { thisRef, sh, key -> + val actionKey = "$key.action" + val control = RvmDisplayableControl(debounceInterval) + thisRef.run { + control.action.setValue(sh[actionKey] ?: RvmDisplayableControl.Action.Hide) + control.action.viewFlowable + .subscribe { sh[actionKey] = it } + .autoDispose() + } + control +} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmInputControl.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmInputControl.kt new file mode 100644 index 0000000..106f95c --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmInputControl.kt @@ -0,0 +1,172 @@ +package com.alexdeww.reactiveviewmodel.widget + +import android.text.* +import android.view.View +import android.widget.EditText +import androidx.lifecycle.SavedStateHandle +import com.alexdeww.reactiveviewmodel.core.* +import com.alexdeww.reactiveviewmodel.core.annotation.RvmBinderDslMarker +import com.alexdeww.reactiveviewmodel.core.annotation.RvmDslMarker +import com.alexdeww.reactiveviewmodel.core.utils.RvmPropertyDelegate +import com.google.android.material.textfield.TextInputLayout +import kotlin.properties.ReadOnlyProperty + +typealias FormatterAction = (text: String) -> String + +class RvmInputControl internal constructor( + initialText: String, + private val hideErrorOnUserInput: Boolean, + formatter: FormatterAction?, + initialEnabled: Boolean, + initialVisibility: Visibility +) : RvmBaseVisualControl( + initialValue = initialText, + initialEnabled = initialEnabled, + initialVisibility = initialVisibility +) { + + init { + dataInternal.valueChangesHook = formatter + } + + val error by RVM.state() + + override fun onDataValueChanged(newValue: String) { + if (hideErrorOnUserInput) error.consumer.accept("") + super.onDataValueChanged(newValue) + } + + override fun getBinder(rvmViewComponent: RvmViewComponent): Binder = Binder(rvmViewComponent) + + inner class Binder internal constructor( + rvmViewComponent: RvmViewComponent + ) : BaseBinder(rvmViewComponent) { + + override val control: RvmBaseVisualControl get() = this@RvmInputControl + + @RvmBinderDslMarker + fun bindTo( + editText: EditText, + bindError: Boolean = false, + bindEnable: Boolean = true, + bindVisible: Boolean = true + ) = bindTo( + view = editText, + editText = editText, + actionOnError = { editText.error = it }, + bindError = bindError, + bindEnable = bindEnable, + bindVisible = bindVisible + ) + + @RvmBinderDslMarker + fun bindTo( + textInputLayout: TextInputLayout, + bindError: Boolean = false, + bindEnable: Boolean = true, + bindVisible: Boolean = true + ) = bindTo( + view = textInputLayout, + editText = textInputLayout.editText!!, + actionOnError = { textInputLayout.error = it }, + bindError = bindError, + bindEnable = bindEnable, + bindVisible = bindVisible + ) + + @Suppress("LongParameterList") + private fun RvmInputControl.bindTo( + view: View, + editText: EditText, + actionOnError: (String) -> Unit, + bindError: Boolean = false, + bindEnable: Boolean = true, + bindVisible: Boolean = true + ) { + var textWatcher: TextWatcher? = null + bindTo( + view = view, + bindEnable = bindEnable, + bindVisible = bindVisible, + onValueChanged = { newValue -> + val editable = editText.text + if (editable != null && !newValue.contentEquals(editable)) { + val ss = SpannableString(newValue) + TextUtils.copySpansFrom(editable, 0, ss.length, null, ss, 0) + editable.replace(0, editable.length, ss) + } + }, + onActiveAction = { + if (bindError) addSource(error.liveData) { actionOnError.invoke(it) } + textWatcher = onTextChangedWatcher { changeValueConsumer.accept(it.toString()) } + editText.addTextChangedListener(textWatcher) + }, + onInactiveAction = { + if (bindError) removeSource(error.liveData) + textWatcher?.let { editText.removeTextChangedListener(it) } + textWatcher = null + } + ) + } + + } + +} + +@Suppress("unused") +@RvmDslMarker +fun RVM.inputControl( + initialText: String = "", + hideErrorOnUserInput: Boolean = true, + formatter: FormatterAction? = null, + initialEnabled: Boolean = true, + initialVisibility: RvmBaseVisualControl.Visibility = RvmBaseVisualControl.Visibility.VISIBLE +): ReadOnlyProperty = RvmPropertyDelegate.def { + RvmInputControl( + initialText = initialText, + hideErrorOnUserInput = hideErrorOnUserInput, + formatter = formatter, + initialEnabled = initialEnabled, + initialVisibility = initialVisibility + ) +} + +@RvmDslMarker +fun SavedStateHandle.inputControl( + initialText: String = "", + hideErrorOnUserInput: Boolean = true, + formatter: FormatterAction? = null, + initialEnabled: Boolean = true, + initialVisibility: RvmBaseVisualControl.Visibility = RvmBaseVisualControl.Visibility.VISIBLE +): ReadOnlyProperty = visualControlDelegate( + initialValue = initialText, + initialEnabled = initialEnabled, + initialVisibility = initialVisibility, + initControl = { value, isEnabled, visibility, _, _ -> + RvmInputControl( + initialText = value, + hideErrorOnUserInput = hideErrorOnUserInput, + formatter = formatter, + initialEnabled = isEnabled, + initialVisibility = visibility + ) + } +) + +private fun onTextChangedWatcher( + action: (CharSequence) -> Unit +): TextWatcher = object : TextWatcher { + + override fun afterTextChanged(s: Editable?) { + /* nothing */ + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + /* nothing */ + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + if (s != null) action.invoke(s) + } + +} diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmRatingControl.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmRatingControl.kt new file mode 100644 index 0000000..3b14a42 --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmRatingControl.kt @@ -0,0 +1,77 @@ +package com.alexdeww.reactiveviewmodel.widget + +import android.annotation.SuppressLint +import android.widget.RatingBar +import androidx.lifecycle.SavedStateHandle +import com.alexdeww.reactiveviewmodel.core.* +import com.alexdeww.reactiveviewmodel.core.annotation.RvmBinderDslMarker +import com.alexdeww.reactiveviewmodel.core.annotation.RvmDslMarker +import com.alexdeww.reactiveviewmodel.core.utils.RvmPropertyDelegate +import kotlin.properties.ReadOnlyProperty + +@SuppressLint("CheckResult") +class RvmRatingControl internal constructor( + initialValue: Float, + initialEnabled: Boolean, + initialVisibility: Visibility +) : RvmBaseVisualControl( + initialValue = initialValue, + initialEnabled = initialEnabled, + initialVisibility = initialVisibility +) { + + override fun getBinder(rvmViewComponent: RvmViewComponent): Binder = Binder(rvmViewComponent) + + inner class Binder internal constructor( + rvmViewComponent: RvmViewComponent + ) : BaseBinder(rvmViewComponent) { + + override val control: RvmBaseVisualControl get() = this@RvmRatingControl + + @RvmBinderDslMarker + fun bindTo( + ratingBar: RatingBar, + bindEnable: Boolean = true, + bindVisible: Boolean = true + ) = bindTo( + view = ratingBar, + bindEnable = bindEnable, + bindVisible = bindVisible, + onValueChanged = { ratingBar.rating = it }, + onActiveAction = { + ratingBar.setOnRatingBarChangeListener { _, rating, _ -> + changeValueConsumer.accept(rating) + } + }, + onInactiveAction = { ratingBar.onRatingBarChangeListener = null } + ) + + } + +} + +@Suppress("unused") +@RvmDslMarker +fun RVM.ratingControl( + initialValue: Float = 0f, + initialEnabled: Boolean = true, + initialVisibility: RvmBaseVisualControl.Visibility = RvmBaseVisualControl.Visibility.VISIBLE +): ReadOnlyProperty = RvmPropertyDelegate.def { + RvmRatingControl( + initialValue = initialValue, + initialEnabled = initialEnabled, + initialVisibility = initialVisibility + ) +} + +@RvmDslMarker +fun SavedStateHandle.ratingControl( + initialValue: Float = 0f, + initialEnabled: Boolean = true, + initialVisibility: RvmBaseVisualControl.Visibility = RvmBaseVisualControl.Visibility.VISIBLE +): ReadOnlyProperty = visualControlDelegate( + initialValue = initialValue, + initialEnabled = initialEnabled, + initialVisibility = initialVisibility, + initControl = rvmControlDefaultConstructor(::RvmRatingControl) +) diff --git a/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmWidgetBindShortcut.kt b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmWidgetBindShortcut.kt new file mode 100644 index 0000000..97dcfe8 --- /dev/null +++ b/reactiveviewmodel/src/main/java/com/alexdeww/reactiveviewmodel/widget/RvmWidgetBindShortcut.kt @@ -0,0 +1,66 @@ +package com.alexdeww.reactiveviewmodel.widget + +import android.app.Dialog +import android.widget.CompoundButton +import android.widget.EditText +import android.widget.RatingBar +import com.alexdeww.reactiveviewmodel.core.RvmViewComponent +import com.alexdeww.reactiveviewmodel.core.annotation.RvmBinderDslMarker +import com.google.android.material.textfield.TextInputLayout + +@RvmBinderDslMarker +interface RvmWidgetBindShortcut : RvmViewComponent { + + @RvmBinderDslMarker + fun RvmCheckControl.bindTo( + compoundButton: CompoundButton, + bindEnable: Boolean = true, + bindVisible: Boolean = true + ) = binder.bindTo(compoundButton, bindEnable, bindVisible) + + @RvmBinderDslMarker + fun RvmDialogControl.bindTo( + dialogHandlerListener: RvmDialogHandlerListener, + dialogCreator: RvmDialogCreator, + ) = binder.bindTo(dialogHandlerListener, dialogCreator) + + @RvmBinderDslMarker + fun RvmDialogControl.bindTo( + dialogCreator: RvmDialogCreator + ) = binder.bindTo(dialogCreator) + + @RvmBinderDslMarker + fun RvmDisplayableControl.bind( + action: RvmDisplayableAction + ) = binder.bind(action) + + @RvmBinderDslMarker + fun RvmDisplayableControl.bind( + onShow: (T) -> Unit, + onHide: () -> Unit + ) = binder.bind(onShow, onHide) + + @RvmBinderDslMarker + fun RvmInputControl.bindTo( + editText: EditText, + bindError: Boolean = false, + bindEnable: Boolean = true, + bindVisible: Boolean = true + ) = binder.bindTo(editText, bindError, bindEnable, bindVisible) + + @RvmBinderDslMarker + fun RvmInputControl.bindTo( + textInputLayout: TextInputLayout, + bindError: Boolean = false, + bindEnable: Boolean = true, + bindVisible: Boolean = true + ) = binder.bindTo(textInputLayout, bindError, bindEnable, bindVisible) + + @RvmBinderDslMarker + fun RvmRatingControl.bindTo( + ratingBar: RatingBar, + bindEnable: Boolean = true, + bindVisible: Boolean = true + ) = binder.bindTo(ratingBar, bindEnable, bindVisible) + +}