Half a year after the last article about comparing RxPM with other presentation patterns, we are ready to present with Jeevuz with Jeevuz the library RxPM - a reactive implementation of the Presentation Model pattern. Let's take a quick overview of the main components of the library and show how to use them.
First, let's look at the general scheme:
Let us turn to the consideration of the main components of the library.
The main objective of RxPM is to describe all the states in PresentationModel and provide the ability to interact with them in a reactive style. Often we need not only to access the state, but also to react to its changes in order to synchronize the view (View). For this, the library has a class State that implements a reactive property.
A reactive property is a type of property that notifies of its changes and provides reactive interfaces for interacting with it.
In the article about the pattern, we said that, we need to describe two properties in order to hide access to the state change from View:
private val inProgressRelay = BehaviorRelay.create() val inProgressObservable = inProgressRelay.hide()
It was one of the annoying moments in the pattern, so we decided to wrap the BehaviorRelay
in the State and provide the observable
and consumer
to interact with it. Now we can write in one line:
val inProgress = State<Boolean>(initialValue = false)
In View we subscribe to state changes:
pm.inProgress.observable.bindTo(progressBar.visibility())
bindTo
- library extension for binding to reactive properties
You can change the state through the consumer , which is available only inside the PresentationModel:
inProgress.consumer.accept(true)
As with the regular property, we can take the current state value:
inProgress.value
The advantage of the reactive property is not only that one can observe its change, but also to link and link it with other reactive properties. So we get a new state that will depend on and respond to the changes of others. For example, you can block a button for the duration of the request to the network:
val inProgress = State(initialValue = false) val buttonEnabled = State(initialValue = true) inProgress.observable .map { progress -> !progress } .subscribe(buttonEnabled.consumer) .untilDestroy()
untilDestroy
is an extension to PresentationModel that adds Disposable
to CompositeDisposable
.
Another example is to enable and disable a button depending on the fill in the fields in the form:
// View: val nameChanges = Action<String>() val phoneChanges = Action<String>() val buttonEnabled = State(initialValue = false) Observable.combineLatest(nameChanges.observable, phoneChanges.observable, BiFunction { name: String, phone: String -> name.isNotEmpty() && phone.isNotEmpty() }) .subscribe(buttonEnabled.consumer) .untilDestroy()
Thus, we can declaratively bind some reactive properties (states) and get others - dependent. This is the essence of reactive programming.
Similar to State, this class encapsulates access to PublishRelay
and is intended to describe user actions, such as pushing buttons, switching, and so on.
val buttonClicks = Action<Unit>() buttonClicks.observable .subscribe { // handle click } .untilDestroy()
It would be a logical question, and not easier to describe a method in PresentationModel, why declare a property and subscribe to it? In some cases this is true. For example, if the action is very simple, such as opening the next screen or calling the model directly. However, if you need to make a request to the network on a click, and at the same time filter clicks during progress, then in this case interaction via Action is preferable. The main advantage of Action is that it does not break the Rx chain. I will explain with an example.
Option with method:
private var requestDisposable: Disposable? = null fun sendRequest() { requestDisposable?.dispose() requestDisposable = model.sendRequest().subscribe() } override fun onDestroy() { super.onDestroy() requestDisposable?.dispose() }
As you can see from the example above, it is necessary to declare Disposable
variable for each request in order to complete the previous request with each new click. And also do not forget to unsubscribe in onDestroy
. This is due to the fact that every time the sendRequest
method is sendRequest
by clicking on a button, a new Rx chain is created.
Option with Action :
buttonClicks.observable .switchMapSingle { model.sendRequest() } .subscribe() .untilDestroy()
Using Action , you only need to initialize the Rx chain once and subscribe to it. In addition, we can use numerous useful Rx operators, such as debounce
, filter
, map
, etc.
For example, consider the delay in the query when entering a string to search:
val searchResult = State<List<Item>>() val searchQuery = Action<String>() searchQuery.observable .debounce(100, TimeUnit.MILLISECONDS) .switchMapSingle { // send request } .subscribe(searchResult.consumer) .untilDestroy()
And in combination with RxBinding , it is even more convenient to link View and PresentationModel:
button.clicks().bindTo(pm.buttonClicks.consumer)
Another important problem is the display of errors and dialogs, or other commands. They are not a state, as they must be executed once. For example, to show the dialogue, the State will not work for us, since each subscription to the State will receive the last value, respectively, each time a new dialogue will be shown. To solve this problem, a Command class was created that implements the desired behavior by encapsulating PublishRelay
.
But what happens if you send a command at a time when View is not yet attached to a PresentationModel? We will lose this team. To prevent this, we have provided a buffer that accumulates commands, while the View is absent, and sends them when the View is bound. When View is bound to PresentationModel, then Command works just like PublishRelay
.
By default, the buffer accumulates an unlimited number of commands, but you can set a specific buffer size:
val errorMessage = Command<String>(bufferSize = 3)
If you want to save only the last command:
val errorMessage = Command<String>(bufferSize = 1)
If you specify 0, then the Command will work as PublishRelay
:
val errorMessage = Command<String>(bufferSize = 0)
Attached to View:
errorMessage.observable().bindTo { message -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show() }
The most illustrative work of the Command is the marble diagram:
By default, the buffer is turned on when binding a View to a PresentationModel. But you can implement your mechanism by specifying the opening / closing observable
.
For example, when working with Google Maps, a sign of readiness to View is not only binding to the PresentationModel, but also the readiness of the map. The library already has a ready command for working with the map:
val moveToLocation = mapCommand<LatLng>()
We described the basic RxPM primitives: State , Action, and Command , from which the PresentationModel is built. Now let's look at the base class PresentationModel
. It carries out all the main work with the life cycle. In total, we have 4 callbacks:
onCreate
- called when first created, this is a good place to initialize Rx chains and link states.onBind
- called when the View is bound to PresentationModel.onUnbind
— Called when the View is unbound from the PresentationModel.onDestroy
- PresentationModel completes its work. The right place to free up resources.You can also monitor the life cycle through lifecycleObservable
.
For a convenient unsubscribe, there are Disposable
extensions available in PresentationModel
:
protected fun Disposable.untilUnbind() { compositeUnbind.add(this) } protected fun Disposable.untilDestroy() { compositeDestroy.add(this) }
onBind
and onDestroy
are cleared with compositeUnbind
and compositeDestroy
respectively.
Let's look at an example of working with PresentationModel
:
It is necessary to send a request to the network via Pull To Refresh and update the data on the screen, display the progress at the time of the request, and in the case of an error show the user a dialogue with the message.
First you need to determine which states and commands are needed for the View and which custom events we can receive from the View:
class DataPresentationModel( private val dataModel: DataModel ) : PresentationModel() { val data = State<List<Item>>(emptyList()) val inProgress = State(false) val errorMessage = Command<String>() val refreshAction = Action<Unit>() // ... }
Now we need to bind the properties and model in the onCreate
method:
class DataPresentationModel( private val dataModel: DataModel ) : PresentationModel() { // ... override fun onCreate() { super.onCreate() refreshAction.observable // .skipWhileInProgress(inProgress.observable) .flatMapSingle { dataModel.loadData() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) // .bindProgress(inProgress.consumer) .doOnError { errorMessage.consumer.accept("Loading data error") } } .retry() .subscribe(data.consumer) .untilDestroy() // refreshAction.consumer.accept(Unit) } }
Pay attention to the operatorretry
, it is necessary here, because when an error is received, the chain will complete its work and the actions will no longer be processed. The operatorretry
chain in case of an error. But be careful and do not use it if you start the chain from State .
When PresentationModel is designed, it remains only to bind it to the View.
The library already has base classes for implementing PmView : PmSupportActivity
, PmSupportFragment
and PmController
(for users of the Conductor framework). Each of them implements the AndroidPmView
interface and pushes the necessary callbacks to the corresponding delegate, which manages the PresentationModel life cycle and ensures that it is correctly stored during the screen rotation.
Inherit from PmSupportFragment
and implement only two mandatory methods:
providePresentationModel
- called when the PresentationModel is created.onBindPresentationModel
- in this method you need to bind to the PresentationModel properties (use RxBinding and the bindTo
extension). class DataFragment : PmSupportFragment<DataPresentationModel>() { override fun providePresentationModel() = DataPresentationModel(DataModel()) override fun onBindPresentationModel(pm: DataPresentationModel) { pm.inProgress.observable.bindTo(swipeRefreshLayout.refreshing()) pm.data.observable.bindTo { // adapter.setItems(it) } pm.errorMessage.observable.bindTo { // show alert dialog } swipeRefreshLayout.refreshes().bindTo(pm.refreshAction.consumer) } }
bindTo
is a handy extension to AndroidPmView
. Using it, you do not need to worry about unsubscribing from the properties from PresentationModel and switching to the main thread.
To work with Google Maps in the library there are additional base classes: MapPmSupportActivity
, MapPmSupportFragment
and MapPmController
. They add a separate method to bind GoogleMap
:
fun onBindMapPresentationModel(pm: PM, googleMap: GoogleMap)
In this method we can display pins on a map, move and animate a location, etc.
So far, we have only considered a one-way change of State , when the PresentationModel changes state, and View subscribes to it. But quite often there is a need to change the state from two sides. The classic example is an input field: its value can be changed by both the user and PresentationModel, initializing with the initial value or formatting the input. This bundle is called bilateral databing. Let us show in the diagram how it is implemented in RxPM:
The user enters the text the listener triggers the change is transferred to Action Action PresentationModel filters and formats the text and substitutes it in State State the changed state receives View View the text is inserted in the input field the listener triggers ➔ the circle closes and the system runs into an endless loop.
We have written an InputControl
class that implements this two-way binding for input fields and solves the looping problem.
We declare in PresentationModel:
val name = inputControl()
We bind to View through the usual bindTo
pm.name bindTo editText
You can also set the formatter:
val name = inputControl( formatter = { it.take(50).capitalize().replace("[^a-zA-Z- ]".toRegex(), "") } )
CheckControl
solves a similar problem of looping and CheckControl
for CheckBox
.
We reviewed the main classes and features of the library. This is not a complete list of features that are in RxPM:
PresentationModel
.PresentationModel
while rotating the screen.PmView
, including for the Conductor.State
, Action
, Command
.InputControl
, CheckContol
, ClickControl
.bindTo
properties through bindTo
and other useful extensions.The library is written in Kotlin and uses RxJava2.
RxPM is already used in several applications in production and has shown stability in its work. But we continue to work on it, there are many ideas for further development and improvement. Version 1.1 recently came out with a very useful feature for navigation, but we'll talk about this in the next article.
Only one article will not be enough to understand the possibilities of RxPM. So try, see the source and examples, ask your questions. We welcome feedback.
RxPM: https://github.com/dmdevgo/RxPM
Sample: https://github.com/dmdevgo/RxPM/tree/develop/sample
Chat in telegrams: https://t.me/Rx_PM
November 24 (this Friday) I will give a mini-report about RxPM on Droidcon Moscow 2017 . Come - let's talk.
Source: https://habr.com/ru/post/342850/
All Articles