From 8a437d7ecc18d2161160cf5d4d509ddd338af525 Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Sat, 29 Feb 2020 21:45:42 +0300 Subject: [PATCH 01/18] Sample refactoring --- sample/build.gradle | 1 + .../me/dmdev/rxpm/sample/main/MainActivity.kt | 13 +- .../me/dmdev/rxpm/sample/main/Messages.kt | 19 +-- .../rxpm/sample/main/ui/base/BackHandler.kt | 1 - .../sample/main/ui/base/ProgressDialog.kt | 2 +- .../dmdev/rxpm/sample/main/ui/base/Screen.kt | 8 +- .../main/ui/base/ScreenPresentationModel.kt | 31 ++--- .../ui/confirmation/CodeConfirmationPm.kt | 55 +++------ .../ui/confirmation/CodeConfirmationScreen.kt | 2 +- .../sample/main/ui/country/ChooseCountryPm.kt | 75 +++++------- .../main/ui/country/ChooseCountryScreen.kt | 4 +- .../dmdev/rxpm/sample/main/ui/main/MainPm.kt | 17 +-- .../rxpm/sample/main/ui/main/MainScreen.kt | 2 +- .../sample/main/ui/phone/AuthByPhonePm.kt | 115 ++++++++---------- .../sample/main/ui/phone/AuthByPhoneScreen.kt | 5 +- 15 files changed, 148 insertions(+), 202 deletions(-) diff --git a/sample/build.gradle b/sample/build.gradle index 843b2dc..504efbb 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -36,6 +36,7 @@ dependencies { // Rx implementation rootProject.rxAndroid2 + implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' // RxBindings implementation rootProject.rxBinding diff --git a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/MainActivity.kt b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/MainActivity.kt index b4ada02..31f32fd 100644 --- a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/MainActivity.kt +++ b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/MainActivity.kt @@ -4,6 +4,7 @@ import android.os.* import androidx.appcompat.app.* import me.dmdev.rxpm.navigation.* import me.dmdev.rxpm.sample.* +import me.dmdev.rxpm.sample.main.AppNavigationMessage.* import me.dmdev.rxpm.sample.main.extensions.* import me.dmdev.rxpm.sample.main.ui.base.* import me.dmdev.rxpm.sample.main.ui.confirmation.* @@ -39,25 +40,25 @@ class MainActivity : AppCompatActivity(), NavigationMessageHandler { when (message) { - is BackMessage -> super.onBackPressed() + is Back -> super.onBackPressed() - is ChooseCountryMessage -> sfm.openScreen(ChooseCountryScreen()) + is ChooseCountry -> sfm.openScreen(ChooseCountryScreen()) - is CountryChosenMessage -> { + is CountryChosen -> { sfm.back() sfm.findScreen()?.onCountryChosen(message.country) } - is PhoneSentSuccessfullyMessage -> sfm.openScreen( + is PhoneSentSuccessfully -> sfm.openScreen( CodeConfirmationScreen.newInstance(message.phone) ) - is PhoneConfirmedMessage -> { + is PhoneConfirmed -> { sfm.clearBackStack() sfm.openScreen(MainScreen(), addToBackStack = false) } - is LogoutCompletedMessage -> { + is LogoutCompleted -> { sfm.clearBackStack() sfm.openScreen(AuthByPhoneScreen(), addToBackStack = false) } diff --git a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/Messages.kt b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/Messages.kt index dc0c45e..24ec376 100644 --- a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/Messages.kt +++ b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/Messages.kt @@ -1,12 +1,13 @@ package me.dmdev.rxpm.sample.main -import me.dmdev.rxpm.navigation.NavigationMessage -import me.dmdev.rxpm.sample.main.util.Country +import me.dmdev.rxpm.navigation.* +import me.dmdev.rxpm.sample.main.util.* -class BackMessage : NavigationMessage - -class ChooseCountryMessage : NavigationMessage -class CountryChosenMessage(val country: Country) : NavigationMessage -class PhoneSentSuccessfullyMessage(val phone: String) : NavigationMessage -class PhoneConfirmedMessage : NavigationMessage -class LogoutCompletedMessage : NavigationMessage \ No newline at end of file +sealed class AppNavigationMessage : NavigationMessage { + object Back : AppNavigationMessage() + object ChooseCountry : AppNavigationMessage() + class CountryChosen(val country: Country) : AppNavigationMessage() + class PhoneSentSuccessfully(val phone: String) : AppNavigationMessage() + object PhoneConfirmed : AppNavigationMessage() + object LogoutCompleted : AppNavigationMessage() +} \ No newline at end of file diff --git a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/BackHandler.kt b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/BackHandler.kt index 912dd00..7ea0b63 100644 --- a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/BackHandler.kt +++ b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/BackHandler.kt @@ -1,6 +1,5 @@ package me.dmdev.rxpm.sample.main.ui.base - interface BackHandler { fun handleBack(): Boolean } \ No newline at end of file diff --git a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/ProgressDialog.kt b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/ProgressDialog.kt index 33ae055..1233ce4 100644 --- a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/ProgressDialog.kt +++ b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/ProgressDialog.kt @@ -15,7 +15,7 @@ class ProgressDialog : DialogFragment() { } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - return Dialog(context!!, R.style.ProgressDialogTheme).apply { + return Dialog(requireContext(), R.style.ProgressDialogTheme).apply { setContentView(ProgressBar(context)) } } diff --git a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/Screen.kt b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/Screen.kt index 0a41cbc..e2cfd15 100644 --- a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/Screen.kt +++ b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/Screen.kt @@ -4,16 +4,14 @@ import android.os.* import android.view.* import androidx.appcompat.app.* import io.reactivex.functions.* +import me.dmdev.rxpm.* import me.dmdev.rxpm.base.* -import me.dmdev.rxpm.passTo -import me.dmdev.rxpm.sample.* +import me.dmdev.rxpm.sample.R import me.dmdev.rxpm.sample.main.extensions.* import me.dmdev.rxpm.widget.* -abstract class Screen : - PmFragment(), - BackHandler { +abstract class Screen : PmFragment(), BackHandler { abstract val screenLayout: Int diff --git a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/ScreenPresentationModel.kt b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/ScreenPresentationModel.kt index c2817c3..a54e31d 100644 --- a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/ScreenPresentationModel.kt +++ b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/ScreenPresentationModel.kt @@ -1,32 +1,25 @@ package me.dmdev.rxpm.sample.main.ui.base -import me.dmdev.rxpm.Action -import me.dmdev.rxpm.PresentationModel -import me.dmdev.rxpm.action -import me.dmdev.rxpm.command -import me.dmdev.rxpm.navigation.NavigationMessage -import me.dmdev.rxpm.navigation.NavigationalPm -import me.dmdev.rxpm.sample.main.BackMessage -import me.dmdev.rxpm.widget.dialogControl +import io.reactivex.functions.* +import me.dmdev.rxpm.* +import me.dmdev.rxpm.navigation.* +import me.dmdev.rxpm.sample.main.AppNavigationMessage.* +import me.dmdev.rxpm.widget.* -abstract class ScreenPresentationModel : PresentationModel(), - NavigationalPm { +abstract class ScreenPresentationModel : PresentationModel(), NavigationalPm { override val navigationMessages = command() val errorDialog = dialogControl() - private val backActionDefault = action() - - open val backAction: Action = backActionDefault - - override fun onCreate() { - super.onCreate() + protected val errorConsumer = Consumer { + errorDialog.show(it?.message ?: "Unknown error") + } - backActionDefault.observable - .subscribe { sendMessage(BackMessage()) } - .untilDestroy() + open val backAction = action { + this.map { Back } + .doOnNext(navigationMessages.consumer) } protected fun sendMessage(message: NavigationMessage) { diff --git a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/confirmation/CodeConfirmationPm.kt b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/confirmation/CodeConfirmationPm.kt index ebed6d6..e37bc2a 100644 --- a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/confirmation/CodeConfirmationPm.kt +++ b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/confirmation/CodeConfirmationPm.kt @@ -1,12 +1,12 @@ package me.dmdev.rxpm.sample.main.ui.confirmation -import io.reactivex.* import me.dmdev.rxpm.* import me.dmdev.rxpm.sample.R -import me.dmdev.rxpm.sample.main.* +import me.dmdev.rxpm.sample.main.AppNavigationMessage.* import me.dmdev.rxpm.sample.main.model.* import me.dmdev.rxpm.sample.main.ui.base.* import me.dmdev.rxpm.sample.main.util.* +import me.dmdev.rxpm.validation.* import me.dmdev.rxpm.widget.* class CodeConfirmationPm( @@ -23,51 +23,34 @@ class CodeConfirmationPm( formatter = { it.onlyDigits().take(CODE_LENGTH) } ) val inProgress = state(false) - val sendButtonEnabled = state(false) - val sendAction = action() - - override fun onCreate() { - super.onCreate() + val sendButtonEnabled = state(false) { + code.text.observable.map { it.length == CODE_LENGTH } + } - val codeFilledAction = code.text.observable - .filter { it.length == CODE_LENGTH } - .distinctUntilChanged() + private val codeFilled = code.text.observable + .filter { it.length == CODE_LENGTH } + .distinctUntilChanged() + .map { Unit } - Observable.merge(sendAction.observable, codeFilledAction) + val sendClicks = action { + this.mergeWith(codeFilled) .skipWhileInProgress(inProgress) .map { code.text.value } - .filter { validateForm() } + .filter { formValidator.validate() } .switchMapCompletable { code -> authModel.sendConfirmationCode(phone, code) .bindProgress(inProgress) - .doOnComplete { sendMessage(PhoneConfirmedMessage()) } - .doOnError { showError(it.message) } + .doOnComplete { sendMessage(PhoneConfirmed) } + .doOnError(errorConsumer) } - .retry() - .subscribe() - .untilDestroy() - - code.text.observable - .map { it.length == CODE_LENGTH } - .subscribe(sendButtonEnabled) - .untilDestroy() - + .toObservable() } - private fun validateForm(): Boolean { - - return when { - code.text.value.isEmpty() -> { - code.error.accept(resourceProvider.getString(R.string.enter_confirmation_code)) - false - } - code.text.value.length < CODE_LENGTH -> { - code.error.accept(resourceProvider.getString(R.string.invalid_confirmation_code)) - false - } - else -> true + private val formValidator = formValidator { + input(code) { + empty(resourceProvider.getString(R.string.enter_confirmation_code)) + minSymbols(CODE_LENGTH, resourceProvider.getString(R.string.invalid_confirmation_code)) } - } } \ No newline at end of file diff --git a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/confirmation/CodeConfirmationScreen.kt b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/confirmation/CodeConfirmationScreen.kt index 40a4a39..592b92e 100644 --- a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/confirmation/CodeConfirmationScreen.kt +++ b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/confirmation/CodeConfirmationScreen.kt @@ -52,7 +52,7 @@ class CodeConfirmationScreen : Screen() { .filter { it == EditorInfo.IME_ACTION_SEND } .map { Unit } ) - .bindTo(pm.sendAction) + .bindTo(pm.sendClicks) } diff --git a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/country/ChooseCountryPm.kt b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/country/ChooseCountryPm.kt index d6ff26e..8c66746 100644 --- a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/country/ChooseCountryPm.kt +++ b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/country/ChooseCountryPm.kt @@ -1,7 +1,7 @@ package me.dmdev.rxpm.sample.main.ui.country import me.dmdev.rxpm.* -import me.dmdev.rxpm.sample.main.* +import me.dmdev.rxpm.sample.main.AppNavigationMessage.* import me.dmdev.rxpm.sample.main.ui.base.* import me.dmdev.rxpm.sample.main.ui.country.ChooseCountryPm.Mode.* import me.dmdev.rxpm.sample.main.util.* @@ -13,44 +13,10 @@ class ChooseCountryPm(private val phoneUtil: PhoneUtil) : ScreenPresentationMode enum class Mode { SEARCH_OPENED, SEARCH_CLOSED } - val countries = state>() - val mode = state(SEARCH_CLOSED) val searchQueryInput = inputControl() + val mode = state(SEARCH_CLOSED) - override val backAction = action() - - val clearAction = action() - val openSearchAction = action() - val countryClicks = action() - - override fun onCreate() { - super.onCreate() - - openSearchAction.observable - .map { SEARCH_OPENED } - .subscribe(mode) - .untilDestroy() - - clearAction.observable - .subscribe { - if (searchQueryInput.text.value.isEmpty()) { - mode.accept(SEARCH_CLOSED) - } else { - searchQueryInput.text.accept("") - } - } - .untilDestroy() - - backAction.observable - .subscribe { - if (mode.value == SEARCH_OPENED) { - mode.accept(SEARCH_CLOSED) - } else { - super.backAction.accept(Unit) - } - } - .untilDestroy() - + val countries = state> { searchQueryInput.text.observable .debounce(100, TimeUnit.MILLISECONDS) .map { query -> @@ -61,13 +27,36 @@ class ChooseCountryPm(private val phoneUtil: PhoneUtil) : ScreenPresentationMode compareValues(c1.name.toLowerCase(), c2.name.toLowerCase()) }) } - .subscribe(countries) - .untilDestroy() + } + + override val backAction = action { + this.doOnNext { + if (mode.value == SEARCH_OPENED) { + mode.accept(SEARCH_CLOSED) + } else { + super.backAction.accept(Unit) + } + } + } - countryClicks.observable - .subscribe { - sendMessage(CountryChosenMessage(it)) + val clearClicks = action { + this.doOnNext { + if (searchQueryInput.text.value.isEmpty()) { + mode.accept(SEARCH_CLOSED) + } else { + searchQueryInput.text.accept("") } - .untilDestroy() + } + } + + val openSearchClicks = action { + this.map { SEARCH_OPENED } + .doOnNext(mode.consumer) + } + + val countryClicks = action { + this.doOnNext { + sendMessage(CountryChosen(it)) + } } } \ No newline at end of file diff --git a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/country/ChooseCountryScreen.kt b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/country/ChooseCountryScreen.kt index caddf05..72820cb 100644 --- a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/country/ChooseCountryScreen.kt +++ b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/country/ChooseCountryScreen.kt @@ -55,8 +55,8 @@ class ChooseCountryScreen : Screen() { pm.searchQueryInput bindTo searchQueryEdit pm.countries bindTo { countriesAdapter.setData(it) } - searchButton.clicks() bindTo pm.openSearchAction - clearButton.clicks() bindTo pm.clearAction + searchButton.clicks() bindTo pm.openSearchClicks + clearButton.clicks() bindTo pm.clearClicks navButton.clicks() bindTo pm.backAction } } diff --git a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/main/MainPm.kt b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/main/MainPm.kt index db6ab18..fb2147e 100644 --- a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/main/MainPm.kt +++ b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/main/MainPm.kt @@ -1,7 +1,7 @@ package me.dmdev.rxpm.sample.main.ui.main import me.dmdev.rxpm.* -import me.dmdev.rxpm.sample.main.* +import me.dmdev.rxpm.sample.main.AppNavigationMessage.* import me.dmdev.rxpm.sample.main.model.* import me.dmdev.rxpm.sample.main.ui.base.* import me.dmdev.rxpm.widget.* @@ -16,13 +16,8 @@ class MainPm(private val authModel: AuthModel) : ScreenPresentationModel() { val logoutDialog = dialogControl() val inProgress = state(false) - val logoutAction = action() - - override fun onCreate() { - super.onCreate() - - logoutAction.observable - .skipWhileInProgress(inProgress) + val logoutClicks = action { + this.skipWhileInProgress(inProgress) .switchMapMaybe { logoutDialog.showForResult(Unit) .filter { it == DialogResult.Ok } @@ -31,10 +26,8 @@ class MainPm(private val authModel: AuthModel) : ScreenPresentationModel() { authModel.logout() .bindProgress(inProgress) .doOnError { showError(it.message) } - .doOnComplete { sendMessage(LogoutCompletedMessage()) } + .doOnComplete { sendMessage(LogoutCompleted) } } - .retry() - .subscribe() - .untilDestroy() + .toObservable() } } \ No newline at end of file diff --git a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/main/MainScreen.kt b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/main/MainScreen.kt index 1aaddb8..b280b09 100644 --- a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/main/MainScreen.kt +++ b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/main/MainScreen.kt @@ -40,7 +40,7 @@ class MainScreen : Screen() { toolbar.itemClicks() .filter { it.itemId == R.id.logoutAction } .map { Unit } - .bindTo(pm.logoutAction) + .bindTo(pm.logoutClicks) } } \ No newline at end of file diff --git a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/phone/AuthByPhonePm.kt b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/phone/AuthByPhonePm.kt index 2f4019b..94cec1f 100644 --- a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/phone/AuthByPhonePm.kt +++ b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/phone/AuthByPhonePm.kt @@ -1,14 +1,15 @@ package me.dmdev.rxpm.sample.main.ui.phone import com.google.i18n.phonenumbers.* -import io.reactivex.* -import io.reactivex.functions.* +import io.reactivex.rxkotlin.Observables.combineLatest import me.dmdev.rxpm.* import me.dmdev.rxpm.sample.R import me.dmdev.rxpm.sample.main.* +import me.dmdev.rxpm.sample.main.AppNavigationMessage.* import me.dmdev.rxpm.sample.main.model.* import me.dmdev.rxpm.sample.main.ui.base.* import me.dmdev.rxpm.sample.main.util.* +import me.dmdev.rxpm.validation.* import me.dmdev.rxpm.widget.* @@ -18,7 +19,6 @@ class AuthByPhonePm( private val authModel: AuthModel ) : ScreenPresentationModel() { - val chosenCountry = state() val phoneNumber = inputControl(formatter = null) val countryCode = inputControl( initialText = "+7", @@ -27,7 +27,7 @@ class AuthByPhonePm( if (code.length > 5) { try { val number = phoneUtil.parsePhone(code) - phoneNumberFocus.accept(Unit) + phoneNumber.focus.accept(true) phoneNumber.textChanges.accept(number.nationalNumber.toString()) "+${number.countryCode}" } catch (e: NumberParseException) { @@ -38,18 +38,7 @@ class AuthByPhonePm( } } ) - - val inProgress = state(false) - val sendButtonEnabled = state(false) - val phoneNumberFocus = command(bufferSize = 1) - - val sendAction = action() - val countryClicks = action() - val chooseCountryAction = action() - - override fun onCreate() { - super.onCreate() - + val chosenCountry = state { countryCode.text.observable .map { val code = it.onlyDigits() @@ -59,67 +48,67 @@ class AuthByPhonePm( Country.UNKNOWN } } - .subscribe(chosenCountry) - .untilDestroy() - - Observable.combineLatest(phoneNumber.textChanges.observable, chosenCountry.observable, - BiFunction { number: String, country: Country -> - phoneUtil.formatPhoneNumber(country, number) - }) - .subscribe(phoneNumber.text) - .untilDestroy() + } + val inProgress = state(false) - Observable.combineLatest(phoneNumber.textChanges.observable, chosenCountry.observable, - BiFunction { number: String, country: Country -> - phoneUtil.isValidPhone(country, number) - }) - .subscribe(sendButtonEnabled) - .untilDestroy() + val sendButtonEnabled = state(false) { + combineLatest( + phoneNumber.textChanges.observable, + chosenCountry.observable + ) { number: String, country: Country -> + phoneUtil.isValidPhone(country, number) + } + } - countryClicks.observable - .subscribe { - sendMessage(ChooseCountryMessage()) - } - .untilDestroy() + val countryClicks = action { + this.map { AppNavigationMessage.ChooseCountry } + .doOnNext(navigationMessages.consumer) + } - chooseCountryAction.observable - .subscribe { - countryCode.textChanges.accept("+${it.countryCallingCode}") - chosenCountry.accept(it) - phoneNumberFocus.accept(Unit) - } - .untilDestroy() + val chooseCountry = action { + this.doOnNext { + countryCode.textChanges.accept("+${it.countryCallingCode}") + chosenCountry.accept(it) + phoneNumber.focus.accept(true) + } + } - sendAction.observable - .skipWhileInProgress(inProgress) - .filter { validateForm() } + val sendClicks = action { + this.skipWhileInProgress(inProgress) + .filter { formValidator.validate() } .map { "${countryCode.text.value} ${phoneNumber.text.value}" } .switchMapCompletable { phone -> authModel.sendPhone(phone) .bindProgress(inProgress) - .doOnComplete { - sendMessage(PhoneSentSuccessfullyMessage(phone)) - } - .doOnError { showError(it.message) } + .doOnComplete { sendMessage(PhoneSentSuccessfully(phone)) } + .doOnError(errorConsumer) } - .retry() - .subscribe() - .untilDestroy() + .toObservable() } - private fun validateForm(): Boolean { + override fun onCreate() { + super.onCreate() - return if (phoneNumber.text.value.isEmpty()) { - phoneNumber.error.accept(resourceProvider.getString(R.string.enter_phone_number)) - false - } else if (!phoneUtil.isValidPhone(chosenCountry.value, phoneNumber.text.value)) { - phoneNumber.error.accept(resourceProvider.getString(R.string.invalid_phone_number)) - false - } else { - true + combineLatest( + phoneNumber.textChanges.observable, + chosenCountry.observable + ) { number: String, country: Country -> + phoneUtil.formatPhoneNumber(country, number) } - + .subscribe(phoneNumber.text) + .untilDestroy() } + private val formValidator = formValidator { + input(phoneNumber) { + empty(resourceProvider.getString(R.string.enter_phone_number)) + valid( + validation = { phoneNumber -> + phoneUtil.isValidPhone(chosenCountry.value, phoneNumber) + }, + errorMessage = resourceProvider.getString(R.string.invalid_phone_number) + ) + } + } } \ No newline at end of file diff --git a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/phone/AuthByPhoneScreen.kt b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/phone/AuthByPhoneScreen.kt index 157d161..737a060 100644 --- a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/phone/AuthByPhoneScreen.kt +++ b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/phone/AuthByPhoneScreen.kt @@ -35,7 +35,6 @@ class AuthByPhoneScreen : Screen() { pm.inProgress bindTo progressConsumer pm.sendButtonEnabled bindTo sendButton::setEnabled - pm.phoneNumberFocus bindTo { phoneNumberEdit.requestFocus() } countryName.clicks() bindTo pm.countryClicks @@ -46,12 +45,12 @@ class AuthByPhoneScreen : Screen() { .filter { it == EditorInfo.IME_ACTION_SEND } .map { Unit } ) - .bindTo(pm.sendAction) + .bindTo(pm.sendClicks) } fun onCountryChosen(country: Country) { - country passTo presentationModel.chooseCountryAction + country passTo presentationModel.chooseCountry } override fun onResume() { From ed5f3d5d0d988dac25d1873517a8f595f35a904b Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Sun, 1 Mar 2020 20:54:49 +0300 Subject: [PATCH 02/18] Rewritten readme --- README.md | 165 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 99 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 412224f..aa532b7 100644 --- a/README.md +++ b/README.md @@ -26,64 +26,61 @@ dependencies { implementation 'me.dmdev.rxpm:rxpm:$latest_version' // RxBinding (optional) - implementation 'com.jakewharton.rxbinding2:rxbinding-kotlin:$latest_version' + implementation 'com.jakewharton.rxbinding3:rxbinding:$latest_version' - // Conductor (if you use it) - implementation 'com.bluelinelabs:conductor:$latest_version' } ``` ### Create a Presentation Model class and define reactive properties ```kotlin -class DataPresentationModel( - private val dataModel: DataModel -) : PresentationModel() { - - val data = State>(emptyList()) - val inProgress = State(false) - val errorMessage = Command() - val refreshAction = Action() - - override fun onCreate() { - super.onCreate() - - refreshAction.observable - .skipWhileInProgress(inProgress.observable) - .flatMapSingle { - dataModel.loadData() - .subscribeOn(Schedulers.io()) - .bindProgress(inProgress.consumer) - .doOnError { - errorMessage.consumer.accept("Loading data error") - } - } - .retry() - .subscribe(data.consumer) - .untilDestroy() - - refreshAction.consumer.accept(Unit) // first loading on create +class CounterPm : PresentationModel() { + + companion object { + const val MAX_COUNT = 10 + } + + val count = state(initialValue = 0) + + val minusButtonEnabled = state { + count.observable.map { it > 0 } + } + + val plusButtonEnabled = state { + count.observable.map { it < MAX_COUNT } + } + + val minusButtonClicks = action { + this.filter { count.value > 0 } + .map { count.value - 1 } + .doOnNext(count.consumer) + } + + val plusButtonClicks = action { + this.filter { count.value < MAX_COUNT } + .map { count.value + 1 } + .doOnNext(count.consumer) } } ``` ### Bind to the PresentationModel properties ```kotlin -class DataFragment : PmSupportFragment() { +class CounterActivity : PmActivity() { - override fun providePresentationModel() = DataPresentationModel(DataModel()) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_counter) + } - override fun onBindPresentationModel(pm: DataPresentationModel) { + override fun providePresentationModel() = CounterPm() - pm.inProgress.observable bindTo swipeRefreshLayout.refreshing() + override fun onBindPresentationModel(pm: CounterPm) { - pm.data.observable bindTo { - // adapter.setItems(it) - } + pm.count bindTo { counterText.text = it.toString() } + pm.minusButtonEnabled bindTo minusButton::setEnabled + pm.plusButtonEnabled bindTo plusButton::setEnabled - pm.errorMessage.observable bindTo { - // show Snackbar - } - - swipeRefreshLayout.refreshes() bindTo pm.refreshAction.consumer + minusButton.clicks() bindTo pm.minusButtonClicks + plusButton.clicks() bindTo pm.plusButtonClicks } } ``` @@ -97,21 +94,21 @@ PresentationModel instance is automatically retained during configuration change Lifecycle callbacks: - `onCreate()` — Called when the PresentationModel is created. Initialize your Rx chains in this method. - `onBind()` — Called when the View binds to the PresentationModel. +- `onResume` - Called when the View resumes and begins to receive updates from states and commands. +- `onPause` - Called when the View pauses. At this point, states and commands stops emitting to the View and turn on internal buffer until the View resumes again. - `onUnbind()` — Called when the View unbinds from the PresentationModel. - `onDestroy()` — Called when the PresentationModel is being destroyed. Dispose all subscriptions in this method. What's more you can observe lifecycle changes via `lifecycleObservable`. -Also the useful extensions of the *Disposable* are available to make lifecycle handling easier: `untilUnbind` and `untilDestroy`. +Also the useful extensions of the *Disposable* are available to make lifecycle handling easier: `untilPause`,`untilUnbind` and `untilDestroy`. ### PmView -The library has several predefined PmView implementations: `PmSupportActivity`, `PmSupportFragment` and `PmController` (for [Conductor](https://github.com/bluelinelabs/Conductor/)'s users). +The library has several predefined PmView implementations: `PmActivity`, `PmFragment`, `PmDialogFragment` and `PmController` (for [Conductor](https://github.com/bluelinelabs/Conductor/)'s users). You have to implement only two methods: 1) `providePresentationModel()` — Create the instance of the PresentationModel. -2) `onBindPresentationModel()` — Bind to the PresentationModel properties in this method. Use the `bindTo` extension and [RxBinding](https://github.com/JakeWharton/RxBinding) for this. - -Also there is variants of these with Google Map integration. +2) `onBindPresentationModel()` — Bind to the PresentationModel properties in this method. Use the `bindTo`, `passTo` extensions and [RxBinding](https://github.com/JakeWharton/RxBinding) to do this. ### State **State** is a reactive property which represents a View state. @@ -119,15 +116,22 @@ It holds the latest value and emits it on binding. For example, **State** can be In the PresentationModel: ```kotlin -val inProgress = State(false) +val inProgress = state(false) ``` -Change the value through the consumer: +Change the value: ```kotlin -inProgress.consumer.accept(true) +inProgress.accept(true) ``` Observe changes in the View: ```kotlin -pm.inProgress.observable bindTo progressBar.visibility() +pm.inProgress bindTo progressBar.visibility() +``` +Usually there is already a data source or the state is derived from other states. In this case, it’s convenient to describe the rx-chain using lambda like as shown bellow: +```kotlin +// Disable a button during the request +val buttonEnabled = state(false) { + inProgress.observable.map { progress -> !progress } +} ``` ### Action @@ -136,19 +140,50 @@ It's mostly used for receiving events from the View, such as clicks. In the View: ```kotlin -button.clicks() bindTo pm.buttonClicks.consumer +button.clicks() bindTo pm.buttonClicks ``` In the PresentationModel: ```kotlin -val buttonClicks = Action() +val buttonClicks = action() +// Subscribe in onCreate buttonClicks.observable .subscribe { // handle click } .untilDestroy() ``` +Typically, the Action triggers an asynchronous operations, such as a request to backend. In this case, the rx-chain will may throw an exception and app will crash. We can handle errors in `subscribe`, but this is not enough. After the first failure, the chain will be completed and stop processing clicks. Therefore, the correct handling involves the use of the `retry` operator and looks as follows: + +```kotlin +val buttonClicks = action() + +// Subscribe in onCreate +buttonClicks.observable + .skipWhileInProgress(inProgress) // filter clicks during the request + .switchMapSingle { + requestInteractor() + .bindProgress(inProgress) + .doOnSuccess { // handle result } + .doOnError { // handel error } + } + .retry() + .subscribe() + .untilDestroy() +``` +Often forget about it. Therefore, we added the ability to describe the rx-chain of `Action` in a lambda when the it is declared. This improves readability and eliminates boilerplate code: +```kotlin +val buttonClicks = action() { + this.skipWhileInProgress(inProgress) // filter clicks during the request + .switchMapSingle { + requestInteractor() + .bindProgress(inProgress) + .doOnSuccess { // handle result } + .doOnError { // handel error } + } +} +``` ### Command **Command** is the reactive property which represents a command to the View. @@ -160,12 +195,12 @@ val errorMessage = Command() ``` Show some message in the View: ```kotlin -pm.errorMessage.observable bindTo { message -> +pm.errorMessage bindTo { message -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } ``` -When the View is unbound from the PresentationModel, **Command** collects all received values and emits them on binding: +When the View is pauses, **Command** collects all received values and emits them on resume: ![Command](/docs/images/bwu.png) @@ -202,17 +237,15 @@ enum class DialogResult { EXIT, CANCEL } val dialogControl = dialogControl() -val backButtonClicks = Action() - -backButtonClicks.observable - .switchMapMaybe { - dialogControl.showForResult("Do you really want to exit?") - } - .filter { it == DialogResult.EXIT } - .subscribe { - // close application - } - .untilDestroy() +val backButtonClicks = action() { + this.switchMapMaybe { + dialogControl.showForResult("Do you really want to exit?") + } + .filter { it == DialogResult.EXIT } + .doOnNext { + // close application + } +} ``` Bind the `dialogControl` to AlertDialog in the View: From 9902d48dd0ee0b7b36c44123ec98fa3f7d48332f Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Sun, 1 Mar 2020 21:18:53 +0300 Subject: [PATCH 03/18] Change command buffer image --- README.md | 2 +- docs/images/bwp.png | Bin 0 -> 67385 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 docs/images/bwp.png diff --git a/README.md b/README.md index aa532b7..3ae7f8d 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ pm.errorMessage bindTo { message -> When the View is pauses, **Command** collects all received values and emits them on resume: -![Command](/docs/images/bwu.png) +![Command](/docs/images/bwp.png) ## Controls diff --git a/docs/images/bwp.png b/docs/images/bwp.png new file mode 100644 index 0000000000000000000000000000000000000000..90ad6864def18dd77c9c2544666c1e72d7e9f327 GIT binary patch literal 67385 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%0Akpdp9w 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 Date: Sun, 1 Mar 2020 21:25:59 +0300 Subject: [PATCH 04/18] Fix code mistakes --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3ae7f8d..3fbdea2 100644 --- a/README.md +++ b/README.md @@ -165,8 +165,8 @@ buttonClicks.observable .switchMapSingle { requestInteractor() .bindProgress(inProgress) - .doOnSuccess { // handle result } - .doOnError { // handel error } + .doOnSuccess { /* handle result */ } + .doOnError { /* handel error */ } } .retry() .subscribe() @@ -174,13 +174,13 @@ buttonClicks.observable ``` Often forget about it. Therefore, we added the ability to describe the rx-chain of `Action` in a lambda when the it is declared. This improves readability and eliminates boilerplate code: ```kotlin -val buttonClicks = action() { +val buttonClicks = action { this.skipWhileInProgress(inProgress) // filter clicks during the request .switchMapSingle { requestInteractor() .bindProgress(inProgress) - .doOnSuccess { // handle result } - .doOnError { // handel error } + .doOnSuccess { /* handle result */ } + .doOnError { /* handel error */ } } } ``` @@ -237,13 +237,13 @@ enum class DialogResult { EXIT, CANCEL } val dialogControl = dialogControl() -val backButtonClicks = action() { +val backButtonClicks = action { this.switchMapMaybe { dialogControl.showForResult("Do you really want to exit?") } .filter { it == DialogResult.EXIT } .doOnNext { - // close application + // close application } } ``` From e8ea8de8c03ba8164320b81d78eb08c3000476a5 Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Sun, 1 Mar 2020 21:58:29 +0300 Subject: [PATCH 05/18] Add form validation block --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index 3fbdea2..7d9cb82 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,48 @@ pm.dialogControl bindTo { message, dialogControl -> } ``` +### Form Validation + +Validating forms is now easy. Create the `FormValidator` using DSL to check `InputControls` and `CheckControls`: +```kotlin +val validateButtonClicks = action { + doOnNext { formValidator.validate() } +} + +private val formValidator = formValidator { + + input(name) { + empty("Input Name") + } + + input(email, required = false) { + pattern(ANDROID_EMAIL_PATTERN, "Invalid e-mail address") + } + + input(phone, validateOnFocusLoss = true) { + valid(phoneUtil::isValidPhone, "Invalid phone number") + } + + input(password) { + empty("Input Password") + minSymbols(6, "Minimum 6 symbols") + pattern( + regex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[\\d]).{6,}\$", + errorMessage = "The password must contain a large and small letters, numbers." + ) + } + + input(confirmPassword) { + empty("Confirm Password") + equalsTo(password, "Passwords do not match") + } + + check(termsCheckBox) { + acceptTermsOfUse.accept("Please accept the terms of use") + } +} +``` + ## Sample The [sample](https://github.com/dmdevgo/RxPM/tree/develop/sample) shows how to use RxPM in practice. From 613a57e218f6b27687c878c4800b24828426308f Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Sun, 1 Mar 2020 22:13:22 +0300 Subject: [PATCH 06/18] Add paging and loading block --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 7d9cb82..327c619 100644 --- a/README.md +++ b/README.md @@ -305,6 +305,10 @@ private val formValidator = formValidator { } ``` +## Paging and Loading +In almost every application, we have pagination and data loading. And also we must correctly handle screen states. +We recommend using the library [RxPagingLoading](https://github.com/MobileUpLLC/RxPagingLoading). The solution is based on the usage of [Unidirectional Data Flow](https://en.wikipedia.org/wiki/Unidirectional_Data_Flow_(computer_science)) pattern and well compatible with RxPM. + ## Sample The [sample](https://github.com/dmdevgo/RxPM/tree/develop/sample) shows how to use RxPM in practice. From 990b8ae19239373709bc3efea4634631b459ccba Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Sun, 1 Mar 2020 22:16:05 +0300 Subject: [PATCH 07/18] Small fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 327c619..41f0596 100644 --- a/README.md +++ b/README.md @@ -318,7 +318,7 @@ The [sample](https://github.com/dmdevgo/RxPM/tree/develop/sample) shows how to u You can test PresentationModel in the same way as any other class with RxJava (using TestObserver, Mockito, other). The only difference is that you have to change it's lifecycle state while testing. And **PmTestHelper** allows you to do that. -Note that Command passes events only when PM is in the BINDED state. +Note that Command passes events only when PM is in the RESUMED state. ## License From ec9f9eda9bf6587b889ca2723c8b6da2e77af516 Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Mon, 2 Mar 2020 13:25:48 +0300 Subject: [PATCH 08/18] Fix readme --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 41f0596..abf8fd2 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ class CounterPm : PresentationModel() { } } ``` +In this sample initialisation of the states and actions done in their blocks, but you also can do it in onCreate() or other callbacks. Don't forget to use untilDestroy() or other similar extension. ### Bind to the PresentationModel properties ```kotlin class CounterActivity : PmActivity() { @@ -126,7 +127,7 @@ Observe changes in the View: ```kotlin pm.inProgress bindTo progressBar.visibility() ``` -Usually there is already a data source or the state is derived from other states. In this case, it’s convenient to describe the rx-chain using lambda like as shown bellow: +Usually there is already a data source or the state is derived from other states. In this case, it’s convenient to describe this using lambda like as shown below: ```kotlin // Disable a button during the request val buttonEnabled = state(false) { @@ -154,6 +155,8 @@ buttonClicks.observable } .untilDestroy() ``` + +#### Action initialisation block to avoid mistakes Typically, the Action triggers an asynchronous operations, such as a request to backend. In this case, the rx-chain will may throw an exception and app will crash. We can handle errors in `subscribe`, but this is not enough. After the first failure, the chain will be completed and stop processing clicks. Therefore, the correct handling involves the use of the `retry` operator and looks as follows: ```kotlin @@ -172,7 +175,7 @@ buttonClicks.observable .subscribe() .untilDestroy() ``` -Often forget about it. Therefore, we added the ability to describe the rx-chain of `Action` in a lambda when the it is declared. This improves readability and eliminates boilerplate code: +But often people forget about it. Therefore, we added the ability to describe the rx-chain of `Action` in it's initialisation block. This improves readability and eliminates boilerplate code: ```kotlin val buttonClicks = action { this.skipWhileInProgress(inProgress) // filter clicks during the request From 4dcedcb46b63a835d12f8a535d6a96b1d11061cc Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Tue, 3 Mar 2020 16:31:18 +0300 Subject: [PATCH 09/18] Fix readme --- README.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index abf8fd2..6c2f04d 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ class CounterPm : PresentationModel() { } } ``` -In this sample initialisation of the states and actions done in their blocks, but you also can do it in onCreate() or other callbacks. Don't forget to use untilDestroy() or other similar extension. +In this sample the initialisation of states and actions is done in their own blocks, but it's also possible to do it in `onCreate()` or other callbacks. Don't forget to use `untilDestroy()` or other similar extension. ### Bind to the PresentationModel properties ```kotlin class CounterActivity : PmActivity() { @@ -96,7 +96,7 @@ Lifecycle callbacks: - `onCreate()` — Called when the PresentationModel is created. Initialize your Rx chains in this method. - `onBind()` — Called when the View binds to the PresentationModel. - `onResume` - Called when the View resumes and begins to receive updates from states and commands. -- `onPause` - Called when the View pauses. At this point, states and commands stops emitting to the View and turn on internal buffer until the View resumes again. +- `onPause` - Called when the View pauses. At this point, states and commands stop emitting to the View and turn on internal buffer until the View resumes again. - `onUnbind()` — Called when the View unbinds from the PresentationModel. - `onDestroy()` — Called when the PresentationModel is being destroyed. Dispose all subscriptions in this method. @@ -127,7 +127,7 @@ Observe changes in the View: ```kotlin pm.inProgress bindTo progressBar.visibility() ``` -Usually there is already a data source or the state is derived from other states. In this case, it’s convenient to describe this using lambda like as shown below: +Usually there is a data source already or the state is derived from other states. In this case, it’s convenient to describe this using lambda as shown below: ```kotlin // Disable a button during the request val buttonEnabled = state(false) { @@ -157,7 +157,7 @@ buttonClicks.observable ``` #### Action initialisation block to avoid mistakes -Typically, the Action triggers an asynchronous operations, such as a request to backend. In this case, the rx-chain will may throw an exception and app will crash. We can handle errors in `subscribe`, but this is not enough. After the first failure, the chain will be completed and stop processing clicks. Therefore, the correct handling involves the use of the `retry` operator and looks as follows: +Typically, some Action triggers an asynchronous operation, such as a request to backend. In this case, the rx-chain may throw an exception and app will crash. It's possible to handle errors in the subscribe block, but this is not enough. After the first failure, the chain will be terminated and stop processing clicks. Therefore, the correct handling involves the use of the `retry` operator and looks as follows: ```kotlin val buttonClicks = action() @@ -230,7 +230,6 @@ pm.checked bindTo checkBox ``` ### Dialogs - The DialogControl is a component make possible the interaction with the dialogs in reactive style. It manages the lifecycle and the state of the dialog. Just bind your Dialog object (eg. AlertDialog) to the DialogControl. No need in DialogFragment anymore. @@ -267,7 +266,6 @@ pm.dialogControl bindTo { message, dialogControl -> ``` ### Form Validation - Validating forms is now easy. Create the `FormValidator` using DSL to check `InputControls` and `CheckControls`: ```kotlin val validateButtonClicks = action { @@ -309,9 +307,8 @@ private val formValidator = formValidator { ``` ## Paging and Loading -In almost every application, we have pagination and data loading. And also we must correctly handle screen states. -We recommend using the library [RxPagingLoading](https://github.com/MobileUpLLC/RxPagingLoading). The solution is based on the usage of [Unidirectional Data Flow](https://en.wikipedia.org/wiki/Unidirectional_Data_Flow_(computer_science)) pattern and well compatible with RxPM. - +In almost every application, there are pagination and data loading. What's more, we have to handle screen states correctly. +We recommend using the library [RxPagingLoading](https://github.com/MobileUpLLC/RxPagingLoading). The solution is based on the usage of [Unidirectional Data Flow](https://en.wikipedia.org/wiki/Unidirectional_Data_Flow_(computer_science)) pattern and is perfectly compatible with RxPM. ## Sample The [sample](https://github.com/dmdevgo/RxPM/tree/develop/sample) shows how to use RxPM in practice. From e8a08f5653dcae32a349b1d7221775e19542919c Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Tue, 3 Mar 2020 16:36:33 +0300 Subject: [PATCH 10/18] Fix readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6c2f04d..994862e 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Lifecycle callbacks: - `onUnbind()` — Called when the View unbinds from the PresentationModel. - `onDestroy()` — Called when the PresentationModel is being destroyed. Dispose all subscriptions in this method. -What's more you can observe lifecycle changes via `lifecycleObservable`. +What's more, you can observe lifecycle changes via `lifecycleObservable`. Also the useful extensions of the *Disposable* are available to make lifecycle handling easier: `untilPause`,`untilUnbind` and `untilDestroy`. @@ -129,7 +129,7 @@ pm.inProgress bindTo progressBar.visibility() ``` Usually there is a data source already or the state is derived from other states. In this case, it’s convenient to describe this using lambda as shown below: ```kotlin -// Disable a button during the request +// Disable the button during the request val buttonEnabled = state(false) { inProgress.observable.map { progress -> !progress } } @@ -203,7 +203,7 @@ pm.errorMessage bindTo { message -> } ``` -When the View is pauses, **Command** collects all received values and emits them on resume: +When the View is paused, **Command** collects all received values and emits them on resume: ![Command](/docs/images/bwp.png) From 26e215dfd7fa78c911f605fe6e54d13cff1582c1 Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Tue, 3 Mar 2020 18:54:46 +0300 Subject: [PATCH 11/18] Add info about state diff strategy --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 994862e..8a68ad9 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,8 @@ val buttonEnabled = state(false) { } ``` +In order to optimize the state update and to avoid unnecessary rendering on the view you can add a `DiffStrategy` in the `State`. By default, the `DiffByEquals` strategy is used. It's suitable for primitives and simple date classes, whereas `DiffByReference` is better to use for collections(like List). + ### Action **Action** is the reactive property which represents the user actions. It's mostly used for receiving events from the View, such as clicks. From 4f1bef35bc24957d20fe3ddad5ca99bc47686ba6 Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Sat, 18 Jul 2020 00:57:57 +0300 Subject: [PATCH 12/18] Update libraries, fix PmController --- build.gradle | 14 +++++++------- gradle/wrapper/gradle-wrapper.properties | 4 ++-- .../main/kotlin/me/dmdev/rxpm/base/PmController.kt | 13 +++++++------ .../me/dmdev/rxpm/delegate/PmControllerDelegate.kt | 14 +++++++------- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/build.gradle b/build.gradle index 710dfd3..302ef21 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,11 @@ buildscript { - ext.kotlinVersion = '1.3.61' + ext.kotlinVersion = '1.3.72' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.1' + classpath 'com.android.tools.build:gradle:4.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" } } @@ -28,20 +28,20 @@ ext { targetSdkVersion = 29 rxBindingVersion = '3.1.0' - annotation = 'androidx.annotation:annotation:1.1.0' - appCompat = 'androidx.appcompat:appcompat:1.2.0-alpha02' - materialDesign = 'com.google.android.material:material:1.2.0-alpha05' + annotation = 'androidx.annotation:annotation:1.2.0-alpha01' + appCompat = 'androidx.appcompat:appcompat:1.3.0-alpha01' + materialDesign = 'com.google.android.material:material:1.3.0-alpha01' kotlinStdlib = "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - rxJava2 = "io.reactivex.rxjava2:rxjava:2.2.18" + rxJava2 = "io.reactivex.rxjava2:rxjava:2.2.19" rxRelay2 = "com.jakewharton.rxrelay2:rxrelay:2.1.1" rxAndroid2 = "io.reactivex.rxjava2:rxandroid:2.1.1" rxBinding = "com.jakewharton.rxbinding3:rxbinding:$rxBindingVersion" rxBindingAppCompat = "com.jakewharton.rxbinding3:rxbinding-appcompat:$rxBindingVersion" - conductor = "com.bluelinelabs:conductor:3.0.0-rc2" + conductor = "com.bluelinelabs:conductor:3.0.0-rc5" junitKotlin = "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion" mockitoKotlin = 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9f8fc83..25d9068 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Feb 29 16:08:15 MSK 2020 +#Sat Jul 18 00:41:11 MSK 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/rxpm/src/main/kotlin/me/dmdev/rxpm/base/PmController.kt b/rxpm/src/main/kotlin/me/dmdev/rxpm/base/PmController.kt index 40d09f2..7b2af30 100644 --- a/rxpm/src/main/kotlin/me/dmdev/rxpm/base/PmController.kt +++ b/rxpm/src/main/kotlin/me/dmdev/rxpm/base/PmController.kt @@ -1,12 +1,13 @@ package me.dmdev.rxpm.base -import android.os.* -import com.bluelinelabs.conductor.* -import me.dmdev.rxpm.* -import me.dmdev.rxpm.delegate.* +import android.os.Bundle +import com.bluelinelabs.conductor.Controller +import me.dmdev.rxpm.PmView +import me.dmdev.rxpm.PresentationModel +import me.dmdev.rxpm.delegate.PmControllerDelegate /** - * Predefined [Conductor's Controller][RestoreViewOnCreateController] implementing the [PmView][PmView]. + * Predefined [Conductor's Controller][Controller] implementing the [PmView][PmView]. * * Just override the [providePresentationModel] and [onBindPresentationModel] methods and you are good to go. * @@ -15,7 +16,7 @@ import me.dmdev.rxpm.delegate.* * See this class's source code for the example. */ abstract class PmController(args: Bundle? = null) : - RestoreViewOnCreateController(args), + Controller(args), PmView { @Suppress("LeakingThis") diff --git a/rxpm/src/main/kotlin/me/dmdev/rxpm/delegate/PmControllerDelegate.kt b/rxpm/src/main/kotlin/me/dmdev/rxpm/delegate/PmControllerDelegate.kt index 32a854c..d86195e 100644 --- a/rxpm/src/main/kotlin/me/dmdev/rxpm/delegate/PmControllerDelegate.kt +++ b/rxpm/src/main/kotlin/me/dmdev/rxpm/delegate/PmControllerDelegate.kt @@ -1,10 +1,11 @@ package me.dmdev.rxpm.delegate -import android.view.* -import com.bluelinelabs.conductor.* -import me.dmdev.rxpm.* -import me.dmdev.rxpm.base.* -import me.dmdev.rxpm.navigation.* +import android.view.View +import com.bluelinelabs.conductor.Controller +import me.dmdev.rxpm.PmView +import me.dmdev.rxpm.PresentationModel +import me.dmdev.rxpm.base.PmController +import me.dmdev.rxpm.navigation.ControllerNavigationMessageDispatcher /** * Delegate for the [Controller] that helps with creation and binding of @@ -27,10 +28,9 @@ class PmControllerDelegate(pmController: C) val presentationModel: PM get() = commonDelegate.presentationModel init { - pmController.addLifecycleListener(object : Controller.LifecycleListener() { + pmController.addLifecycleListener(object : Controller.LifecycleListener { override fun preCreateView(controller: Controller) { - super.preCreateView(controller) if (!created) { commonDelegate.onCreate(null) created = true From 44f00f58ee369ce421337d001e0d331583c06ea6 Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Sat, 18 Jul 2020 01:06:44 +0300 Subject: [PATCH 13/18] Fix docs --- rxpm/src/main/kotlin/me/dmdev/rxpm/State.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rxpm/src/main/kotlin/me/dmdev/rxpm/State.kt b/rxpm/src/main/kotlin/me/dmdev/rxpm/State.kt index 8bbb116..658d0ec 100644 --- a/rxpm/src/main/kotlin/me/dmdev/rxpm/State.kt +++ b/rxpm/src/main/kotlin/me/dmdev/rxpm/State.kt @@ -1,12 +1,12 @@ package me.dmdev.rxpm -import android.annotation.* -import com.jakewharton.rxrelay2.* -import io.reactivex.* -import io.reactivex.android.schedulers.* -import io.reactivex.functions.* -import io.reactivex.schedulers.* -import me.dmdev.rxpm.util.* +import android.annotation.SuppressLint +import com.jakewharton.rxrelay2.BehaviorRelay +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.functions.Consumer +import io.reactivex.schedulers.Schedulers +import me.dmdev.rxpm.util.BufferSingleValueWhileIdleOperator /** * Reactive property for the [view's][PmView] state. @@ -163,7 +163,7 @@ interface DiffStrategy { /** * Compares the old and the new values. - * @return [true] if both values ​​are identical or [false] if they are different. + * @return true if both values ​​are identical or false if they are different. */ fun areTheSame(new: T, old: T): Boolean From bb2acfe1e36109681b4b6fdb26075261b7e3603b Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Sat, 18 Jul 2020 01:17:12 +0300 Subject: [PATCH 14/18] Fix pm test --- .../me/dmdev/rxpm/PresentationModelTest.kt | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/rxpm/src/test/kotlin/me/dmdev/rxpm/PresentationModelTest.kt b/rxpm/src/test/kotlin/me/dmdev/rxpm/PresentationModelTest.kt index 9280076..e86fe85 100644 --- a/rxpm/src/test/kotlin/me/dmdev/rxpm/PresentationModelTest.kt +++ b/rxpm/src/test/kotlin/me/dmdev/rxpm/PresentationModelTest.kt @@ -1,15 +1,22 @@ package me.dmdev.rxpm -import com.nhaarman.mockitokotlin2.* -import io.reactivex.observers.* -import me.dmdev.rxpm.PresentationModel.* +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.spy +import com.nhaarman.mockitokotlin2.verify +import io.reactivex.observers.TestObserver +import me.dmdev.rxpm.PresentationModel.Lifecycle import me.dmdev.rxpm.PresentationModel.Lifecycle.* -import org.junit.* +import me.dmdev.rxpm.util.SchedulersRule +import org.junit.Before +import org.junit.Rule import org.junit.Test -import kotlin.test.* +import kotlin.test.assertEquals +import kotlin.test.assertNull class PresentationModelTest { + @get:Rule val schedulers = SchedulersRule() + private lateinit var lifecycleCallbacks: LifecycleCallbacks private lateinit var pm: TestPm private lateinit var lifecycleObserver: TestObserver From f5f5da1b630ff1c90f4a07c1505a7c3ac229ee28 Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Sat, 18 Jul 2020 01:19:16 +0300 Subject: [PATCH 15/18] Fix deprecation --- rxpm/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rxpm/build.gradle b/rxpm/build.gradle index 28bf29b..ab39b74 100644 --- a/rxpm/build.gradle +++ b/rxpm/build.gradle @@ -75,7 +75,7 @@ apply from: 'bintray.gradle' task sourcesJar(type: Jar) { from android.sourceSets.main.java.srcDirs - classifier = 'sources' + archiveClassifier.set("sources") } artifacts { From 4472f13343187c15442447d21d66e53c1a5bb03e Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Sat, 18 Jul 2020 01:25:57 +0300 Subject: [PATCH 16/18] Fix a code complete action in the sample --- .../ui/confirmation/CodeConfirmationPm.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/confirmation/CodeConfirmationPm.kt b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/confirmation/CodeConfirmationPm.kt index e37bc2a..27d3676 100644 --- a/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/confirmation/CodeConfirmationPm.kt +++ b/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/confirmation/CodeConfirmationPm.kt @@ -1,13 +1,20 @@ package me.dmdev.rxpm.sample.main.ui.confirmation -import me.dmdev.rxpm.* +import me.dmdev.rxpm.action +import me.dmdev.rxpm.bindProgress import me.dmdev.rxpm.sample.R -import me.dmdev.rxpm.sample.main.AppNavigationMessage.* -import me.dmdev.rxpm.sample.main.model.* -import me.dmdev.rxpm.sample.main.ui.base.* -import me.dmdev.rxpm.sample.main.util.* -import me.dmdev.rxpm.validation.* -import me.dmdev.rxpm.widget.* +import me.dmdev.rxpm.sample.main.AppNavigationMessage.PhoneConfirmed +import me.dmdev.rxpm.sample.main.model.AuthModel +import me.dmdev.rxpm.sample.main.ui.base.ScreenPresentationModel +import me.dmdev.rxpm.sample.main.util.ResourceProvider +import me.dmdev.rxpm.sample.main.util.onlyDigits +import me.dmdev.rxpm.skipWhileInProgress +import me.dmdev.rxpm.state +import me.dmdev.rxpm.validation.empty +import me.dmdev.rxpm.validation.formValidator +import me.dmdev.rxpm.validation.input +import me.dmdev.rxpm.validation.minSymbols +import me.dmdev.rxpm.widget.inputControl class CodeConfirmationPm( private val phone: String, @@ -28,7 +35,7 @@ class CodeConfirmationPm( code.text.observable.map { it.length == CODE_LENGTH } } - private val codeFilled = code.text.observable + private val codeFilled = code.textChanges.observable .filter { it.length == CODE_LENGTH } .distinctUntilChanged() .map { Unit } From 28020b0625d7c6e2566a015f526a48ff4c26d2c7 Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Sat, 18 Jul 2020 02:00:46 +0300 Subject: [PATCH 17/18] Update bintray plugin --- rxpm/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rxpm/build.gradle b/rxpm/build.gradle index ab39b74..fdb34db 100644 --- a/rxpm/build.gradle +++ b/rxpm/build.gradle @@ -3,7 +3,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' + classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' } } From 27cf9525afae379f0adc35e8cef83cf8a1bb00d0 Mon Sep 17 00:00:00 2001 From: Dmitriy Gorbunov Date: Sat, 18 Jul 2020 02:01:03 +0300 Subject: [PATCH 18/18] Increase library version --- rxpm/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rxpm/build.gradle b/rxpm/build.gradle index fdb34db..7459a42 100644 --- a/rxpm/build.gradle +++ b/rxpm/build.gradle @@ -63,7 +63,7 @@ ext { bintrayName = 'RxPM' publishedGroupId = 'me.dmdev.rxpm' artifact = 'rxpm' - libraryVersion = '2.1' + libraryVersion = '2.1.1' gitUrl = 'https://github.com/dmdevgo/RxPM' allLicenses = ['MIT'] }