Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
dmdevgo committed Jul 17, 2020
2 parents 960299c + 27cf952 commit 073454e
Show file tree
Hide file tree
Showing 24 changed files with 357 additions and 315 deletions.
221 changes: 151 additions & 70 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,64 +26,62 @@ 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<List<Item>>(emptyList())
val inProgress = State(false)
val errorMessage = Command<String>()
val refreshAction = Action<Unit>()

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<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
```kotlin
class DataFragment : PmSupportFragment<DataPresentationModel>() {
class CounterActivity : PmActivity<CounterPm>() {

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
}
}
```
Expand All @@ -97,59 +95,101 @@ 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 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`.
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.
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:
```kotlin
val inProgress = State<Boolean>(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 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 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:
```kotlin
button.clicks() bindTo pm.buttonClicks.consumer
button.clicks() bindTo pm.buttonClicks
```

In the PresentationModel:
```kotlin
val buttonClicks = Action<Unit>()
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:

```kotlin
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:
```kotlin
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.
Expand All @@ -160,14 +200,14 @@ val errorMessage = Command<String>()
```
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 paused, **Command** collects all received values and emits them on resume:

![Command](/docs/images/bwu.png)
![Command](/docs/images/bwp.png)

## Controls

Expand All @@ -192,7 +232,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.

Expand All @@ -202,17 +241,15 @@ enum class DialogResult { EXIT, CANCEL }

val dialogControl = dialogControl<String, DialogResult>()

val backButtonClicks = Action<Unit>()

backButtonClicks.observable
.switchMapMaybe {
dialogControl.showForResult("Do you really want to exit?")
}
.filter { it == DialogResult.EXIT }
.subscribe {
// close application
}
.untilDestroy()
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:
Expand All @@ -230,6 +267,50 @@ 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<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](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.
Expand All @@ -239,7 +320,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

Expand Down
14 changes: 7 additions & 7 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -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"
}
}
Expand All @@ -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'
Expand Down
Binary file added docs/images/bwp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 073454e

Please sign in to comment.