RxJava meets Data Binding I - Origins
09 Jan 2016This is Part 1 about combining RxJava and Data Binding. This article presents the first iteration and how this pattern came into existence. Part 2 provides an improved internal implementation and approach for handling several practical issues.
RxJava and Android Data Binding both provide mechanisms for subscribing for changes. In many discussions, I find people recommending one against the other. However, I found that using Data Binding and RxJava works quite well.
Before Data Binding
Before data binding, a convenient way for writing View Models was
class CartModel {
final Observable<Float> totalAmount; // Lets say this gets updated through a source that we don't care about
}
class CartViewModel {
final Observable<String> totalAmountText;
CartViewModel(CartModel cartModel) {
totalPrice = cartModel.totalAmount.map { amount -> "$ " + amount.toString() };
}
}
The view (which could be an Activity/Fragment or a custom widget, but that’s not the point), would look like:
class CartView {
TextView totalAmountTextView;
List<Subscription> subscriptions = new ArrayList<>();
void init(CartViewModel cartVM) {
subscriptions.add(cartVM.totalAmountText.subscribe { text -> totalAmountTextView.setText(text) });
}
void onDestroy() {
for (subscription : subscriptions) {
subscription.unsubscribe();
}
}
}
With Data binding
Databinding allows you to bind object fields to almost any property of a view through XML, which means no need to call setter methods like setText(). Instead of calling these methods, we can bind the text property to a field of view model.
This can be achieved as follows:
cart.xml
...
<data>
<variable name="vm" type="com.example.CartViewModel"/>
</data>
<TextView
android:text="{ vm.totalAmountText }"/>
...
// In code which inflates cart, we do:
cartBinding.setVm(cartViewModel);
This is all fine except that databinding expects that CartViewModel must satisfy one of these conditions:
- have a field
totalAmountTextof typeStringOR - have a method
String getTotalAmountText()OR - have a field
totalAmountTextof typeObservableField<String>OR - there are other ways, which aren’t required in this context.
In the first two options, in order to make totalAmountText changes appear on the view, we’ll need to make CartViewModel extend databinding.BaseObservable and invoke notifyPropertyChanged(BR.totalAmountText) whenever it changes. However, this adds boilerplate in the View Model code as we’ll transition from
class CartViewModel {
final Observable<String> totalAmountText;
CartViewModel(CartModel cartModel) {
totalPrice = cartModel.totalAmount.map { amount -> "$ " + amount.toString() };
}
}
to
class CartViewModel {
CartViewModel(CartModel cartModel) {
cartModel.totalAmount.subscribe { amount -> setTotalAmountText("$ " + amount) };
}
String getTotalAmountText() {
return mTotalAmountText;
}
String setTotalAmountText(String totalAmountText) {
mTotalAmountText = totalAmountText;
notifyPropertyChanged(BR.totalAmountText);
}
}
Boilerplate is obviously undesirable. This brings us to option 3 i.e. ObservableField. ObservableField and rx.Observable are very similar in the sense that they allow subscribing for change. We can extend ObservableField to add a constructor which takes Observable as an argument.
class RxObservableField<T> extends ObservableField<T> {
public RxObservableField(Observable<T> source) {
source.subscribe { value -> set(value) };
}
}
With RxObservableField, our view model becomes very neat.
class CartViewModel {
final RxObservableField<String> totalAmountText;
CartViewModel(CartModel cartModel) {
totalPrice = new RxObservableField(cartModel.totalAmount.map { amount -> "$ " + amount.toString() });
}
}
This implementation of RxObservableField is very basic. Few things need to be taken care of:
- Memory Leaks
- Closing subscriptions
What’s next?
- As
RxObservableFieldinternally subscribes to an observable, this subscription should be closed when the view gets destroyed. - When
subscribemethod is called, the reference ofRxObservableFieldinstance goes to the sourceObservable. This might cause a leak. ObservableFieldalso allows consumers to set inner value. Thus, its more similar to aSubject. This would be useful to capture user input, for example inEditText. However, as data binding doesn’t exactly support two way binding, some more work is required on that front.- Another thing is events like
onClickoronTextChanged. It would be awesome to derive anrx.Observablefor these events. However, writing a generic adapter seems tricky at this point.