{"id":16322367,"url":"https://github.com/dmdevgo/rxpm","last_synced_at":"2025-03-16T14:31:03.174Z","repository":{"id":46124354,"uuid":"97994543","full_name":"dmdevgo/RxPM","owner":"dmdevgo","description":"Reactive implementation of Presentation Model pattern in Android.","archived":false,"fork":false,"pushed_at":"2021-10-30T17:14:46.000Z","size":1168,"stargazers_count":182,"open_issues_count":6,"forks_count":18,"subscribers_count":8,"default_branch":"develop","last_synced_at":"2025-03-16T03:04:17.714Z","etag":null,"topics":["android","architecture","mvvm","presentation-model","reactive-properties","rxpm"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dmdevgo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-07-21T23:49:17.000Z","updated_at":"2025-03-03T11:07:08.000Z","dependencies_parsed_at":"2022-09-03T06:51:23.217Z","dependency_job_id":null,"html_url":"https://github.com/dmdevgo/RxPM","commit_stats":null,"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmdevgo%2FRxPM","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmdevgo%2FRxPM/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmdevgo%2FRxPM/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmdevgo%2FRxPM/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dmdevgo","download_url":"https://codeload.github.com/dmdevgo/RxPM/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243882409,"owners_count":20363126,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","architecture","mvvm","presentation-model","reactive-properties","rxpm"],"created_at":"2024-10-10T22:50:40.469Z","updated_at":"2025-03-16T14:31:02.469Z","avatar_url":"https://github.com/dmdevgo.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# RxPM\n\n[![Maven Central](https://img.shields.io/maven-central/v/me.dmdev.rxpm/rxpm/2.1.2)](https://repo1.maven.org/maven2/me/dmdev/rxpm/rxpm/2.1.2) \n[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-RxPM-brightgreen.svg?style=flat)](https://android-arsenal.com/details/1/7089) \n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) \n\nReactive implementation of [Presentation Model](https://martinfowler.com/eaaDev/PresentationModel.html) pattern in Android.\n\nRxPM allows to use the RxJava all the way from the view to the model.  \nThe main advantage of that is the **ability to write UI logic declaratively**.\n\nWe focus on practice, so the library solves most of the typical presentation layer problems.\n\n\u003e Also, see a [multiplatform implementation](https://github.com/dmdevgo/Premo) of the Presentation Model.\n\n### Why PM and not MVVM?\n\nActually the only difference between these two is that PM does'n have automated binding.  \nSo PM name is just more correct for us. However many call it MVVM, so let it be.\n\n### The Diagram\n\n\u003cimg src=\"/docs/images/rxpm_diagram.png\"\u003e\n\n## Usage\n\nAdd the dependency to your build.gradle:\n```gradle\ndependencies {\n\n    implementation 'me.dmdev.rxpm:rxpm:$latest_version'\n    \n    // RxBinding (optional)\n    implementation 'com.jakewharton.rxbinding3:rxbinding:$latest_version'\n    \n}\n```\n\n### Create a Presentation Model class and define reactive properties\n\n```kotlin\nclass CounterPm : PresentationModel() {\n\n    companion object {\n        const val MAX_COUNT = 10\n    }\n\n    val count = state(initialValue = 0)\n\n    val minusButtonEnabled = state {\n        count.observable.map { it \u003e 0 }\n    }\n\n    val plusButtonEnabled = state {\n        count.observable.map { it \u003c MAX_COUNT }\n    }\n\n    val minusButtonClicks = action\u003cUnit\u003e {\n        this.filter { count.value \u003e 0 }\n            .map { count.value - 1 }\n            .doOnNext(count.consumer)\n    }\n\n    val plusButtonClicks = action\u003cUnit\u003e {\n        this.filter { count.value \u003c MAX_COUNT }\n            .map { count.value + 1 }\n            .doOnNext(count.consumer)\n    }\n}\n```\nIn 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.\n\n### Bind to the PresentationModel properties\n\n```kotlin\nclass CounterActivity : PmActivity\u003cCounterPm\u003e() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_counter)\n    }\n\n    override fun providePresentationModel() = CounterPm()\n\n    override fun onBindPresentationModel(pm: CounterPm) {\n\n        pm.count bindTo { counterText.text = it.toString() }\n        pm.minusButtonEnabled bindTo minusButton::setEnabled\n        pm.plusButtonEnabled bindTo plusButton::setEnabled\n\n        minusButton.clicks() bindTo pm.minusButtonClicks\n        plusButton.clicks() bindTo pm.plusButtonClicks\n    }\n}\n```\n\n## Main Components\n\n### PresentationModel\n\nThe PresentationModel stores the state of the View and holds the UI logic.  \nPresentationModel instance is automatically retained during configuration changes. This behavior is provided by the delegate which controls the lifecycle.\n\nLifecycle callbacks:\n- `onCreate()` — Called when the PresentationModel is created. Initialize your Rx chains in this method.\n- `onBind()` — Called when the View binds to the PresentationModel.\n- `onResume` - Called when the View resumes and begins to receive updates from states and commands.\n- `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.\n- `onUnbind()` — Called when the View unbinds from the PresentationModel.\n- `onDestroy()` — Called when the PresentationModel is being destroyed. Dispose all subscriptions in this method.\n\nWhat's more, you can observe lifecycle changes via `lifecycleObservable`.\n\nAlso the useful extensions of the *Disposable* are available to make lifecycle handling easier: `untilPause`,`untilUnbind` and `untilDestroy`.\n\n### PmView\n\nThe library has several predefined PmView implementations: `PmActivity`, `PmFragment`, `PmDialogFragment` and `PmController` (for [Conductor](https://github.com/bluelinelabs/Conductor/)'s users).  \n\nYou have to implement only two methods:\n1) `providePresentationModel()` — Create the instance of the PresentationModel.\n2) `onBindPresentationModel()` — Bind to the PresentationModel properties in this method. Use the `bindTo`, `passTo` extensions and [RxBinding](https://github.com/JakeWharton/RxBinding) to do this.\n\n### State\n\n**State** is a reactive property which represents a View state.  \nIt 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.\n\nIn the PresentationModel:\n```kotlin\nval inProgress = state(false)\n```\nChange the value:\n```kotlin\ninProgress.accept(true)\n```\nObserve changes in the View:\n```kotlin\npm.inProgress bindTo progressBar.visibility()\n```\nUsually 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:\n```kotlin\n// Disable the button during the request\nval buttonEnabled = state(false) {\n    inProgress.observable.map { progress -\u003e !progress }\n}\n```\n\nIn 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).\n\n### Action\n\n**Action** is the reactive property which represents the user actions.  \nIt's mostly used for receiving events from the View, such as clicks.\n\nIn the View:\n```kotlin\nbutton.clicks() bindTo pm.buttonClicks\n```\n\nIn the PresentationModel:\n```kotlin\nval buttonClicks = action\u003cUnit\u003e()\n\n// Subscribe in onCreate\nbuttonClicks.observable\n    .subscribe {\n        // handle click\n    }\n    .untilDestroy()\n```\n\n#### Action initialisation block to avoid mistakes\n\nTypically, 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:\n\n```kotlin\nval buttonClicks = action\u003cUnit\u003e()\n\n// Subscribe in onCreate\nbuttonClicks.observable\n    .skipWhileInProgress(inProgress) // filter clicks during the request\n    .switchMapSingle {\n        requestInteractor()\n            .bindProgress(inProgress)\n            .doOnSuccess { /* handle result */ }\n            .doOnError { /* handel error */ }\n    }\n    .retry()\n    .subscribe()\n    .untilDestroy()\n```\nBut 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:\n```kotlin\nval buttonClicks = action\u003cUnit\u003e {\n    this.skipWhileInProgress(inProgress) // filter clicks during the request\n        .switchMapSingle {\n            requestInteractor()\n                .bindProgress(inProgress)\n                .doOnSuccess { /* handle result */ }\n                .doOnError { /* handel error */ }\n    }\n}\n```\n\n### Command\n\n**Command** is the reactive property which represents a command to the View.  \nIt can be used to show a toast or snackbar.\n\nDefine it in the PresentationModel:\n```kotlin\nval errorMessage = Command\u003cString\u003e()\n```\nShow some message in the View:\n```kotlin\npm.errorMessage bindTo { message -\u003e\n    Toast.makeText(context, message, Toast.LENGTH_SHORT).show()\n}\n```\n\nWhen the View is paused, **Command** collects all received values and emits them on resume:\n\n![Command](/docs/images/bwp.png)\n\n## Controls\n\n### Two-way Data Binding\n\nFor the cases of two-way data binding (eg. input field text changes) the library has predefined [Сontrols](https://github.com/dmdevgo/RxPM/tree/develop/rxpm/src/main/kotlin/me/dmdev/rxpm/widget).\n\nIn the PresentationModel:\n```kotlin\nval name = inputControl(\n    formatter = {\n        it.take(50).capitalize().replace(\"[^a-zA-Z- ]\".toRegex(), \"\")\n    }\n)\n\nval checked = checkControl()\n```\n\nIn the View:\n```kotlin\npm.name bindTo editText\npm.checked bindTo checkBox\n```\n\n### Dialogs\n\nThe DialogControl is a component make possible the interaction with the dialogs in reactive style.  \nIt manages the lifecycle and the state of the dialog. Just bind your Dialog object (eg. AlertDialog) to the DialogControl. No need in DialogFragment anymore.\n\nHere is an example of the dialog to confirm exit from the application:\n```kotlin\nenum class DialogResult { EXIT, CANCEL }\n\nval dialogControl = dialogControl\u003cString, DialogResult\u003e()\n\nval backButtonClicks = action\u003cUnit\u003e {\n    this.switchMapMaybe {\n            dialogControl.showForResult(\"Do you really want to exit?\")\n        }\n        .filter { it == DialogResult.EXIT }\n        .doOnNext {\n            // close application\n        }\n}\n```\n\nBind the `dialogControl` to AlertDialog in the View:\n```kotlin\npm.dialogControl bindTo { message, dialogControl -\u003e\n    AlertDialog.Builder(context)\n        .setMessage(message)\n        .setPositiveButton(\"Exit\") { _, _ -\u003e\n            dialogControl.sendResult(DialogResult.EXIT)\n        }\n        .setNegativeButton(\"Cancel\") { _, _ -\u003e\n            dialogControl.sendResult(DialogResult.CANCEL)\n        }\n        .create()\n}\n```\n\n### Form Validation\n\nValidating forms is now easy. Create the `FormValidator` using DSL to check `InputControls` and `CheckControls`:\n```kotlin\nval validateButtonClicks = action\u003cUnit\u003e {\n    doOnNext { formValidator.validate() }\n}\n    \nprivate val formValidator = formValidator {\n\n    input(name) {\n        empty(\"Input Name\")\n    }\n    \n    input(email, required = false) {\n        pattern(ANDROID_EMAIL_PATTERN, \"Invalid e-mail address\")\n    }\n    \n    input(phone, validateOnFocusLoss = true) {\n        valid(phoneUtil::isValidPhone, \"Invalid phone number\")\n    }\n    \n    input(password) {\n        empty(\"Input Password\")\n        minSymbols(6, \"Minimum 6 symbols\")\n        pattern(\n            regex = \"^(?=.*[a-z])(?=.*[A-Z])(?=.*[\\\\d]).{6,}\\$\",\n            errorMessage = \"The password must contain a large and small letters, numbers.\"\n        )\n    }\n    \n    input(confirmPassword) {\n        empty(\"Confirm Password\")\n        equalsTo(password, \"Passwords do not match\")\n    }\n    \n    check(termsCheckBox) {\n        acceptTermsOfUse.accept(\"Please accept the terms of use\")\n    }\n}\n```\n\n## Paging and Loading\n\nIn almost every application, there are pagination and data loading. What's more, we have to handle screen states correctly.\nWe 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.\n\n## Sample\n\nThe [sample](https://github.com/dmdevgo/RxPM/tree/develop/sample) shows how to use RxPM in practice.\n\n## How to test PM?\n\nYou can test PresentationModel in the same way as any other class with RxJava (using TestObserver, Mockito, other).  \nThe only difference is that you have to change it's lifecycle state while testing. And **PmTestHelper** allows you to do that.\n\nNote that Command passes events only when PM is in the RESUMED state.\n\n## Thanks\n\nThanks for contributing:\n[@Jeevuz](https://github.com/Jeevuz)\n[@sdelaysam](https://github.com/sdelaysam)\n[@vchernyshovnullgr](https://github.com/vchernyshovnullgr)\n[@aasitnikov](https://github.com/aasitnikov)\n[@mochalovv](https://github.com/mochalovv)\n\n## License\n\n```\nThe MIT License (MIT)\n\nCopyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)\n                    and Vasili Chyrvon (vasili.chyrvon@gmail.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmdevgo%2Frxpm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdmdevgo%2Frxpm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmdevgo%2Frxpm/lists"}