Skip to content

Commit

Permalink
Merge pull request #3 from agrosner/develop
Browse files Browse the repository at this point in the history
1.0.0
  • Loading branch information
agrosner authored Dec 12, 2017
2 parents 0b3acb8 + 631b5e2 commit 50f8d1e
Show file tree
Hide file tree
Showing 24 changed files with 762 additions and 568 deletions.
180 changes: 180 additions & 0 deletions Bindings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Supported Bindings

Currently we support three kinds of bindings:
1. `oneWay` -> handle changes from `ViewModel` to `View`
2. `twoWay` -> handles changes in both directions between `ViewModel` <-> `View`
3. `oneWayToSource` -> handles changes from `View` to `ViewModel`

## One Way Bindings

`oneWay` bindings handle changes from a `Observable` or functional expression on a specific `View`.

The changes from an `ObservableField` come directly from the instance, while changes
from an expression need explicit wiring to determine for which property it came from.

### Flow
expression or `ObservableField.value` -> `Output` -> `View` -> `View` property set from `Output`

The expression or `ObservableField.value` is considered the `Input` which can get transformed in the `Output` phase,
which then applies to the `View` via Setter methods. This library provides a few default out-of-the-box methods as conveniences. These are fully extensible and customizable.

The expression syntax is required to register to changes on a specific `KProperty` or `MyViewModelClass::someField`. Then whenever we want that expression to get reevaluated, we need to `notifyChange(MyViewModelClass::someField)` on the registered parent object, or `ViewModel`.

```kotlin

textView {
bindSelf(MyViewModelClass::someField)
{ viewModel.someField }
.toText(this)
}

```

Just by specifying this alone is not enough to get changes from that expression. We also need to, whenever that property is `set`, notify to our parent `viewModel` that a change occurred:

```kotlin

class MyViewModelClass : BaseObservable() {

var someField = ""
set(value) {
field = value
notifyChange(this::someField)
}
}

```

When we call `notifyChange` that expression `{ viewModel.someField }` runs again
and the `textView` (in this example) updates its `text` with the result of that expression.

The expression syntax is very useful when we want to update UI based on our field's value.
Take, in an e-commerce app we want to display the number of items in the cart. The value is an `Int` but we want to update the UI whenever that count of items changes. We define an `ObservableField<Int>` and bind it to the view:

```kotlin

textView {
bind { viewModel.count }
.on { string(R.string.someFormattedString, plural(R.plural.somePlural, it)) } // helper methods for `View.context`
.toText(this)
}

```

So now whenever we call `viewModel.count.value = newValue`, the expression reruns and the UI updates!

To specify this example on a custom text setter:
```kotlin

textView {
bind { viewModel.count }
.on { string(R.string.someFormattedString, plural(R.plural.somePlural, it)) } // helper methods for `View.context`
.toView(this, { view, value ->
text = value
})
}

```


## One Way To Source Bindings

`oneWayToSource` is the reverse of `oneWay`. It specifies that we want changes from the UI to send back data to our `ViewModel` via an `ObservableField` or expression.

Since Views will send back results to the expression or `ObservableField`, registering is a little different. We must first bind a `View` via a `ViewRegister`. `ViewRegister` are an abstract class that handle registering and unregistering specific listeners on `View`. For example, a `OnTextChangedRegister()` adds a `TextWatcher` on a `TextView` and receives callbacks when text changes. That result is then passed along to the expression or `ObservableField`.


### Flow
`View` -> `Output` -> `Input` -> expression or `ObservableField.value`

The `ViewRegister` knows how to convert the view's data to an `Output`, then the `on` clause specifies a potential conversion into another type `Input`.
Then the `Input` gets sent to the expression or `ObservableField.value` for updating.
For example:

```kotlin

textView {
bind(this)
.onSelf() // String updates from `text`
.toObservable { it.name }

// or custom update method
bindSelf(this).to { vm, input, view ->
// viewmodel might be null
vm?.let { vm ->
// assign data to the viewmodel
vm.name = input
}
}
}

```

For convenience, we provide a set of default `bind()` and `ViewRegister` that you can use out of the box. These are completely extensible and customizable.

Without convenience methods, we must create a `ViewRegister`:

```kotlin
class MyOnTextChangedRegister : ViewRegister<TextView, String>(), TextWatcher {

override fun registerView(view: TextView) = view.addTextChangedListener(this)

override fun deregisterFromView(view: TextView) = view.removeTextChangedListener(this)

override fun afterTextChanged(s: Editable?) = Unit

override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
notifyChange(s?.toString()) // pass changes to to listeners so data can update.
}

override fun getValue(view: TextView) = view.text.toString() // specifies how to convert view data out
}

```

Then pass it into the call to `bind`:

```kotlin
textView {
bind(this, MyOnTextChangedRegister())
.onSelf() // String updates from `text`
.to { viewModel.count }
}
```

## Two Way Bindings

`twoWay` bindings are slightly more complex and complicated. It specifies that an expression or `ObservableField` and `View`'s data are synchronized. We only allow one such binding per `KProperty` or `ObservableField` to prevent a cycle of updates occurring in the UI.

We start off the binding the same way as a `oneWay` (`twoWay` extends off of `oneWay`) and then specify we want it `twoWay` and complete the reverse assignment. Any default, out-of-the-box `oneWay` binding on a `View` will only update `View` when the value is different than the current value. This prevents update cycles that could occur in a `twoWay`.

### Flow
expression or `ObservableField.value` -> `Output` -> `View` -> `View` property set -> `Output` -> expression or `ObservableField.value` is set if changed.

When a `View` changes, it notifies the expression or `ObservableField`. When the expression or `ObservableField` changes, they notify the `View`. Both the `View` and `ObservableField` have mechanisms in place to only change and notify when their value changes so that a cycle in this flow doesn't happen.

To register a `twoWay` binding on an `ObservableField` that relates to a user inputting data for an address:

```kotlin
editText {
bindSelf { viewModel.address }.toText(this)
.twoWay().toFieldFromText()
}
```

This means that any changes from user input get populated into the `address` property, and any changes (say from API call) are passed along to the `editText` as well.

The `toFieldFromText()` is a convenience method that assigns an `ObservableField` the value from the `editText` in reverse. There are more convenience methods.

To use expressions without conveniences:

```kotlin
editText {
bindSelf(MyViewModelClass::address) { viewModel.address }.toText(this)
.twoWay().toInput(OnTextChangedRegister()) { viewValue ->
viewModel.address.value = it ?: viewModel.address.defaultValue // if null, set non-null default if we'd like.
}
}
```
84 changes: 84 additions & 0 deletions Examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# KBinding By Example:

Create a `BindingHolder` to contain our bindings:

```kotlin
val holder = BindingHolder(viewModel)
```

Bind the value of `ObservableField<String>` to the UI `TextView`:

```kotlin
// one way binding on Observable fields
holder.bindSelf { viewModel.name }.toText(textView)
```

Bind the field value of a normal `String` to the visibility `Int`:
```kotlin
// one way binding on non observables
holder.bind(ViewModel::name) { it.name }
.onIsNotNullOrEmpty() // if null or empty return false
.toShowHideView(someView) // if true show, if false hide.
```

Bind both changes of `ObservableField<String>` and `EditText` value changes:
```kotlin
// two way binding on observable that synchronizes text and value changes.
holder.bindSelf { it.name }
.toText(input)
.twoWay()
.toFieldFromText()
```

Bind changes of `ObservableField<Boolean>` and a `CheckBox` change together:
```kotlin
// two way binding that synchronizes compoundbutton / checkbox changes
holder.bindSelf { it.selected }
.toOnCheckedChange(checkbox)
.twoWay()
.toFieldFromCompound()
```

Back changes in UI from a `TextView`/`EditText` with an `ObservableField<String>`:
```kotlin
// binds input changes from the view to the name property.
holder.bind(textView)
.onSelf()
.to { it.name }
```

Back changes in UI from a `TextView`/`EditText` to a non-observable property:
```kotlin
// binds input changes from the view to the name property (non observable).
holder.bind(textView)
.onSelf()
.to { input, view -> it.name = input}
```

We support swapping top-level `ViewModel` in the `BindingHolder`:
```kotlin
holder.viewModel = viewModel // set the ViewModel (no restriction and could be a `Presenter`)
holder.viewModel = null // support null `ViewModel` too!
// if bound will reevaluate the bindings.
```

By default bindings are __not__ executed when the `ViewModel` is `null`. If you wish to supply default values or execute when it is `null`, use:
```kotlin
// if normal binding, a default value for field is used and the expression is not evaluated.
// in this case it's executed always
holder.bindNullable(ViewModel::name) { it?.name }
.onSelf()
.toText(textView)
```

When done, cleanup bindings to prevent memory leaks! (`BindingComponent` for Anko does the cleanup for you when calling `destroyView()`)
```kotlin
holder.unbindAll() // when done, unbind
```

We can also _easily_ turn off individual bindings as needed, just hold a reference to it:
```kotlin
val binding = holder.bindSelf(textView).toObservable { it.name }
binding.unbind() // can turn off binding as needed

```
116 changes: 116 additions & 0 deletions GettingStartedAnko.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
## Getting Started Using Anko

KBinding works best with [Anko](https://github.com/Kotlin/anko), but can be used by other consumers.

First, we need a ViewModel or object to send data to our observableBindings.

By default normal properties, when their value changes, will not propagate those changes
to our observableBindings. So we have a couple of options. First we must extend `BaseObservable`.

```kotlin

class UserViewModel(var name: String = "") : BaseObservable()

```

The base class of `BaseObservable` by default handles propagating changes of the ViewModel
and fields to the observableBindings when notified. In order to notify changes to the parent `ViewModel`,
we have three options:

1. Override a fields `setter` and notify changes to our `BaseObservable`:

```kotlin
var name = ""
set(value) {
field = value
notifyChange(this::name)
}

```
2. Delegate the property to our `Observable` implementation:

```kotlin
var name: String by observable("")

```
This auto-registers changes to the field to the parent `ViewModel` to know when value changes.

3. Make the field `Observable` (preferred).

```kotlin

var name = observable("")

```

Option 3 is the simplest as we can bind directly to the field and notify its changes. Option 2 is the best for outside consumers so it's treated like a real field. Option 1 should be used for efficiency in implementation.

When binding, option (1) requires us to explicitly notify the parent on change of the field.
Option (1) and (2) also requires us to specify the field in the binding:
```kotlin

holder.bindSelf(UserViewModel::name) { it.name }
.toText(this)

```

Option (3) is preferred since we can then easily bind data changes without explicit reference
to the `KProperty`:

```kotlin

holder.bindSelf { it.name }.toText(this)

```

We use `it` in the binding, not the direct reference to `viewModel` or top-level object in the view because if that `ViewModel` changes, the bindings will reference a stale object!

### Create the UI

Have our components that we use in `Anko` extend `BindingComponent<Activity, ViewModel>` for convenience collection and disposal of the observableBindings:

```kotlin
class MainActivityLayout(mainActivityViewModel: MainActivityViewModel)
: BindingComponent<MainActivity, MainActivityViewModel>(mainActivityViewModel) {
```

Instead of overridding `createView()`, we override `createViewWithBindings()`. This is
so we internally can bind all created observableBindings after view is created.

Then to bind views:

```kotlin

override fun createViewWithBindings(ui: AnkoContext<MainActivity>): View {
return with(ui) {
textView {
bindSelf(UserViewModel::name) { it.name }
.toText(this)
.twoWay()
.toFieldFromText()
}
}
}

```

The `BindingComponent` is backed by the `BindingHolder`, which collects and manages
the observableBindings.

If we do not unbind the `BindingHolder`, it will lead to memory leaks of all of the observableBindings. You need to explicitly call `unbind()` when calling the `BindingHolder` directly, or `destroyView()` if using the `BindingComponent`:

```kotlin
bindingHolder.unbind()

```

In an `Activity.onDestroy()` (or `Fragment.onDestroyView()`)

```kotlin

override fun onDestroy() {
super.onDestroy()
component.destroyView()
}

```
Loading

0 comments on commit 50f8d1e

Please sign in to comment.