Giter Club home page Giter Club logo

rxpm's Introduction

RxPM

Maven Central Android Arsenal License: MIT

Reactive implementation of Presentation Model pattern in Android.

RxPM allows to use the RxJava all the way from the view to the model.
The main advantage of that is the ability to write UI logic declaratively.

We focus on practice, so the library solves most of the typical presentation layer problems.

Also, see a multiplatform implementation of the Presentation Model.

Why PM and not MVVM?

Actually the only difference between these two is that PM does'n have automated binding.
So PM name is just more correct for us. However many call it MVVM, so let it be.

The Diagram

Usage

Add the dependency to your build.gradle:

dependencies {

    implementation 'me.dmdev.rxpm:rxpm:$latest_version'
    
    // RxBinding (optional)
    implementation 'com.jakewharton.rxbinding3:rxbinding:$latest_version'
    
}

Create a Presentation Model class and define reactive properties

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<Unit> {
        this.filter { count.value > 0 }
            .map { count.value - 1 }
            .doOnNext(count.consumer)
    }

    val plusButtonClicks = action<Unit> {
        this.filter { count.value < MAX_COUNT }
            .map { count.value + 1 }
            .doOnNext(count.consumer)
    }
}

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

class CounterActivity : PmActivity<CounterPm>() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_counter)
    }

    override fun providePresentationModel() = CounterPm()

    override fun onBindPresentationModel(pm: CounterPm) {

        pm.count bindTo { counterText.text = it.toString() }
        pm.minusButtonEnabled bindTo minusButton::setEnabled
        pm.plusButtonEnabled bindTo plusButton::setEnabled

        minusButton.clicks() bindTo pm.minusButtonClicks
        plusButton.clicks() bindTo pm.plusButtonClicks
    }
}

Main Components

PresentationModel

The PresentationModel stores the state of the View and holds the UI logic.
PresentationModel instance is automatically retained during configuration changes. This behavior is provided by the delegate which controls the lifecycle.

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

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.

PmView

The library has several predefined PmView implementations: PmActivity, PmFragment, PmDialogFragment and PmController (for 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, passTo extensions and RxBinding to do this.

State

State is a reactive property which represents a View state.
It holds the latest value and emits it on binding. For example, State can be used to represent a progress of the http-request or some data that can change in time.

In the PresentationModel:

val inProgress = state(false)

Change the value:

inProgress.accept(true)

Observe changes in the View:

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:

// Disable the button during the request
val buttonEnabled = state(false) {
    inProgress.observable.map { progress -> !progress }
}

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.

In the View:

button.clicks() bindTo pm.buttonClicks

In the PresentationModel:

val buttonClicks = action<Unit>()

// Subscribe in onCreate
buttonClicks.observable
    .subscribe {
        // handle click
    }
    .untilDestroy()

Action initialisation block to avoid mistakes

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:

val buttonClicks = action<Unit>()

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

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:

val buttonClicks = action<Unit> {
    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.
It can be used to show a toast or snackbar.

Define it in the PresentationModel:

val errorMessage = Command<String>()

Show some message in the View:

pm.errorMessage bindTo { message ->
    Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}

When the View is paused, Command collects all received values and emits them on resume:

Command

Controls

Two-way Data Binding

For the cases of two-way data binding (eg. input field text changes) the library has predefined Сontrols.

In the PresentationModel:

val name = inputControl(
    formatter = {
        it.take(50).capitalize().replace("[^a-zA-Z- ]".toRegex(), "")
    }
)

val checked = checkControl()

In the View:

pm.name bindTo editText
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.

Here is an example of the dialog to confirm exit from the application:

enum class DialogResult { EXIT, CANCEL }

val dialogControl = dialogControl<String, DialogResult>()

val backButtonClicks = action<Unit> {
    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:

pm.dialogControl bindTo { message, dialogControl ->
    AlertDialog.Builder(context)
        .setMessage(message)
        .setPositiveButton("Exit") { _, _ ->
            dialogControl.sendResult(DialogResult.EXIT)
        }
        .setNegativeButton("Cancel") { _, _ ->
            dialogControl.sendResult(DialogResult.CANCEL)
        }
        .create()
}

Form Validation

Validating forms is now easy. Create the FormValidator using DSL to check InputControls and CheckControls:

val validateButtonClicks = action<Unit> {
    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")
    }
}

Paging and Loading

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. The solution is based on the usage of Unidirectional Data Flow pattern and is perfectly compatible with RxPM.

Sample

The sample shows how to use RxPM in practice.

How to test PM?

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 RESUMED state.

Thanks

Thanks for contributing: @Jeevuz @sdelaysam @vchernyshovnullgr @aasitnikov @mochalovv

License

The MIT License (MIT)

Copyright (c) 2017-2021 Dmitriy Gorbunov ([email protected])
                    and Vasili Chyrvon ([email protected])

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

rxpm's People

Contributors

aasitnikov avatar dmdevgo avatar jeevuz avatar mochalovv avatar sdelaysam avatar vchernyshovnullgr avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rxpm's Issues

In fragment onBindPresentationModel calls before onViewCreated

onViewCreated often uses to initialize views via findViewById but in version 2.0 this initialization should be done or in onCreateView or call initialization code before super. onViewCreated. I guess this changes can broke a lot code after updating to new version and overriding onCreateView not so convenient as onViewCreated.

Maybe there is some way to return previous behavior?

Type projection for Action

Hey guys.
I was trying to user single action as a common sink for multiple buttons:

Observable.just(1).bindTo(pm.someAction)
Observable.just(1.0).bindTo(pm.someAction)

where someAction is
val someAction = action<Number>()
And I've got the compile time error, which can be fixed by

infix fun <T> Observable<T>.bindTo(action: Action<in T>) { //type projection here
    with(action.pm) {
        [email protected](AndroidSchedulers.mainThread())
            .subscribe(action.consumer)
            .untilUnbind()
    }
}

Have you done it on purpose, so any `action` has strict type parameter?

Packages refactoring discussion

@dmdevgo let's discuss following changes:

  1. rename package base to view because it contains views as per pattern.
  2. move PmStore out of delegate package because it relates not only to delegates.
  3. rename package widget to control
  4. rename package util to internal because operators is not utils but internal stuff for the library.

PresentationModel.lifecycleObservable and untilDestroy.

PresentationModel.lifecycleObservable won't emit the Lifecycle.DESTROYED if subscribed with added untilDestroy().
It is because of this part:

lifecycleObservable
            .takeUntil { it == Lifecycle.DESTROYED }
            .subscribe {
                when (it) {
...
                    Lifecycle.DESTROYED -> {
                        compositeDestroy.clear()
                        onDestroy()
                    }
                }
...

lifecycleObservable calls the compositeDestroy.clear() and untilDestroy will unsubscribe the source, so our subscription won't get the DESTROYED.

Logically it's ok, but it makes lifecycleObservable less usable in practice. Think we need to change this to get DESTROYED before unsubscribing.

Add PmDialogFragment

In some cases like BottomSheet, dialogs require PresentationModel. I propose to add such component to library.

Make relay and consumer in PresentationModel protected or public

Sometimes there is need to create custom control (for example, for Spinner) and there is a problem with relay in PresentationModel - it's internal and there is no way to write custom control cause relay visible only to the library package. There is the same problem with consumer - it's visible only to the PresentationModel.

Integrated support for permissions processing

RxPM focuses on solving practical problems of the presentation layer. Such features as DialogControl or dispatching of navigation messages save us of many problems and boilerplate code. Processing of permissions is the same problem. Although there are many libraries, like RxPermissions, but many difficulties arise when trying to embed them in the PresentationModel. So I think it would be convenient to have a solution out of the box.

Make onCreate(), onDestroy(), onBind(), onUnbind() @CallSuper

CallSuper is applied to framework lifecycle methods on activities and fragments. It also applies transitively to their subclasses in jetpack/support library. Using CallSuper on lifecycle methods in UI code may be considered a convention at that point.
I find RxPM's behavior a little conuterintuitive, because the lack of CallSuper on a method is basically a hint to omit a super call. That led to some hard to find bugs when trying out RxPM at my workplace. The issue was solved by using an application-wise BasePresentationModel with the annotation, but it still caused some confusion.

Method onBindPresentationModel not call

In the normal case, everything works and the method is called.
But specifically in my case, I have a FragmentStatePagerAdapter where there are identical fragments that are attached to the presenter model and when you scroll through the fragments, some are properly destroyed to save resources, and when you switch to the fragment that was previously destroyed, the onBindPresentationModel method is no longer called, and I have to bind in the onActivityCreated method

Зависимость от support design library

Создал проект но не подключил к нему support design library.
Выпадала ошибка: error: cannot access TextInputLayout class file for android.support.design.widget.TextInputLayout not found Consult the following stack trace for details.
После подключения support design library все начало работать нормально.

onActivityResult - onRequestPermissionsResult support in Activity/Controller class

Hi @dmdevgo ,

Sorry for disturbing again but this is important.

RxPm already handles CREATED, BINDED, RESUMED, PAUSED, UNBINDED, DESTROYED.
But onActivityResult and onRequestPermissionsResult not supported. Function results like login etc should handled in onActivityResult. And with RxPm, business logic can not be in Controller or Activity, it should be in presenter.

Conductor support onActivityResult is like that: (https://github.com/bluelinelabs/Conductor/blob/develop/conductor/src/main/java/com/bluelinelabs/conductor/Router.java)

Implementation is like below:
`class MainActivity : AppCompatActivity() {

private lateinit var router: Router

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    router.onActivityResult(requestCode, resultCode, data)
}

}`

I think it makes sense to support onActivityResult in PresentationModel according to Router.
What do you think about that?

Thanks in advance.

Testing Command<T> in RxPm PresentationModel

Hi, I want to test my PresentationModel and have the problem with Command.
Thats my test

@Test
    fun `sign in clicked`() {
        pm.signInClicks.consumer.accept(Unit)
        pm.navigationMessages.observable.test().assertValue { it is AppNavigationMessages.SignInOpen }
    }

I noticed that the command depends on the Android Schedulers if the buffer is not zero. I do not want to inject a command to PresentationModel for more readable Pm constructor. What's best practices to test commands in RxPm?

About Conductor Support

Hi all,

FOA thanks for the library.

I am trying to implement RxPM with Conductor. But getting "public final class TestBaseFormController extends me.dmdev.rxpm.base.PmController<com.xxx.xx.presentationmodels.FormValidationPm> { class file for com.bluelinelabs.conductor.RestoreViewOnCreateController not found" error.

My steps:

  • Added implementation 'me.dmdev.rxpm:rxpm:2.1' to build.gradle and synced.

  • Created FormValidationPm class like the sample app.

  • Created Controller like below:

`
class BaseFormController : PmController() {

override fun providePresentationModel(): FormValidationPm {
    return FormValidationPm()
}

override fun onBindPresentationModel(pm: FormValidationPm) {
    TODO("Not yet implemented")
}

}
`

Please let me know if it is not working with Conductor.

Thanks in advance!

Implement operator to replace bufferWhileIdle

The extension works well in most cases, but has several drawbacks:

  • The behavior of the opening / closing buffer is achieved by a non-trivial chain of operators, that degrades performance
  • In some cases, the order of the sequence of commands is violated. For example, when a command is emitted at the time a buffer closes.

Question about unsubscribes

Hello.
In the examples, you have a code in which, when subscribing to Action, there is a call to the untilDestroy helper method when subscribing to Action.
There are also examples in View (Activity/Fragment) where a quick method is used that also unsubscribes from a property.
And I had a question whether I really need to unsubscribe.
Suppose I have a Presentation Model and an action, I subscribe to it, and when the Presentation Model is destroyed, does the garbage collector not clear all the Action resources since there are no references to it outside the life cycle of the Presentation Model.
The same thing about subscribing to a state in a view (activity/fragment).
Maybe my question seems stupid, but I would like to figure it out here.

Navigation message observable lifecycle

Kinda question which may be a bug

fun onCreate() {
 navigationMessagesDisposable = presentationModel.navigationMessages.observable
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe {
                navigationMessagesDispatcher.dispatch(it)
            }
}

fun onDestroy() {
navigationMessagesDisposable.dispose()
}

Posting navigation messages which commit fragment transactions may lead to

 Can not perform this action after onSaveInstanceState

Or I'm missing something?

Unnecessary rxjava operator call

While i was looking through source code of PmExtensions.kt, i noticed that on 115 line there's identity call: .flatMapIterable { it }. I think you can safely replace previous method in call chain(map { }) with flatMapIterable without hurting readability and improve runtime performance a bit.
Also in skipWhileInProgress last two methods use .first and .second properties of Pair, while bufferWhileIdle use destructing declaration for same purposes, which serves better readabily.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.