Share
Embed
Functional MVVM using RxJava and Data Binding
Talk at DroidCon India 2016:
https://droidconin.talkfunnel.com/2016/32-functional-mvvm-using-rxjava-and-android-data-bind
Video:
https://www.youtube.com/watch?v=JaB8SXCSbgg&t=1s
Manas Chaudhari
November 10, 2016
Transcript
97 slides
-
User interfaces • Lot of changing states • Data from
server • Change on user action -
Static World String email = emailEditText.getText(); String phone =
phoneEditText.getText(); // Intermediate boolean emailValid = FormUtils.checkEmail(email); boolean phoneValid = FormUtils.checkPhone(phone); // Outputs String emailError = emailValid ? null : "Invalid Email"; String phoneError = phoneValid ? null : "Invalid Phone"; boolean loginEnabled = emailValid && phoneValid; Real code isn’t so simple. Why? -
Need Listeners emailEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(...) {}
@Override public void onTextChanged(...) {} @Override public void afterTextChanged(Editable s) { checkEmail(s.toString()); } }); void checkEmail(String email) { // Further calculations } -
void checkEmail(String email) { boolean emailValid = FormUtils.checkEmail(email); String emailError
= emailValid ? null : "Invalid Email"; emailEditText.setError(emailError); } -
void checkEmail(String email) { boolean emailValid = FormUtils.checkEmail(email); String emailError
= emailValid ? null : "Invalid Email"; emailEditText.setError(emailError); } phoneEditText.addTextChangedListener(...) void checkPhone(String phone) { boolean phoneValid = FormUtils.checkPhone(phone); String phoneError = phoneValid ? null : "Invalid phone"; phoneEditText.setError(phoneError); } -
String email = emailEditText.getText(); String phone = phoneEditText.getText();
// Intermediate boolean emailValid = FormUtils.checkEmail(email); boolean phoneValid = FormUtils.checkPhone(phone); // Outputs String emailError = emailValid ? null : "Invalid Email"; String phoneError = phoneValid ? null : "Invalid Phone"; boolean loginEnabled = emailValid && phoneValid; -
private boolean emailValid; private boolean phoneValid; void checkEmail(String email)
{ emailValid = FormUtils.checkEmail(email); String emailError = emailValid ? null : "Invalid Email"; emailEditText.setError(emailError); refreshLoginEnabled(); } void checkPhone(CharSequence phone) { phoneValid = FormUtils.checkPhone(phone.toString()); String phoneError = phoneValid ? null : "Invalid phone"; phoneEditText.setError(phoneError); refreshLoginEnabled(); } private void refreshLoginEnabled() { loginButton.setEnabled(phoneValid && emailValid); } -
Problems • Mutation -> refresh chain • Boilerplate code for
connecting data to view -
public class LoginState { // Inputs public final ObservableField<String> email;
public final ObservableField<String> phone; // Outputs public final ReadOnlyField<String> emailError; public final ReadOnlyField<String> phoneError; public final ReadOnlyField<Boolean> loginEnabled; public class LoginState { // Inputs public final ObservableField<String> email; public final ObservableField<String> phone; // Outputs public final ReadOnlyField<String> emailError; public final ReadOnlyField<String> phoneError; public final ReadOnlyField<Boolean> loginEnabled; Solution Preview -
public class LoginState { // Inputs public final ObservableField<String> email;
public final ObservableField<String> phone; // Outputs public final ReadOnlyField<String> emailError; public final ReadOnlyField<String> phoneError; public final ReadOnlyField<Boolean> loginEnabled; Solution Preview Input, Output Fields -
LoginState() { Observable<String> emailObservable = toObservable(email); Observable<String> phoneObservable =
toObservable(phone); Observable<Boolean> emailValid, phoneValid; emailValid = emailObservable.map( email -> FormUtils.checkEmail(email) ); phoneValid = phoneObservable.map( phone -> FormUtils.checkPhone(phone) ); emailError = toField(emailValid.map( emailValid -> emailValid ? null : "Invalid Email"; ); phoneError = toField(phoneValid.map( phoneValid -> phoneValid ? null : "Invalid Phone"; ); loginEnabled = toField(Observable.combineLatest(emailValid, phoneValid, (emailValid, phoneValid) -> emailValid && phoneValid )); } LoginState() { Observable<String> emailObservable = toObservable(email); Observable<String> phoneObservable = toObservable(phone); Observable<Boolean> emailValid, phoneValid; emailValid = emailObservable.map( email -> FormUtils.checkEmail(email) ); phoneValid = phoneObservable.map( phone -> FormUtils.checkPhone(phone) ); emailError = toField(emailValid.map( emailValid -> emailValid ? null : "Invalid Email"; ); phoneError = toField(phoneValid.map( phoneValid -> phoneValid ? null : "Invalid Phone"; ); loginEnabled = toField(Observable.combineLatest(emailValid, phoneValid, (emailValid, phoneValid) -> emailValid && phoneValid )); } -
LoginState() { Observable<String> emailObservable = toObservable(email); Observable<String> phoneObservable =
toObservable(phone); Observable<Boolean> emailValid, phoneValid; emailValid = emailObservable.map( email -> FormUtils.checkEmail(email) ); phoneValid = phoneObservable.map( phone -> FormUtils.checkPhone(phone) ); emailError = toField(emailValid.map( emailValid -> emailValid ? null : "Invalid Email"; ); phoneError = toField(phoneValid.map( phoneValid -> phoneValid ? null : "Invalid Phone"; ); loginEnabled = toField(Observable.combineLatest(emailValid, phoneValid, (emailValid, phoneValid) -> emailValid && phoneValid )); } -
boolean emailValid = FormUtils.checkEmail(email); boolean phoneValid = FormUtils.checkPhone(phone); String
emailError = emailValid ? null : "Invalid Email"; String phoneError = phoneValid ? null : "Invalid Phone"; boolean loginEnabled = emailValid && phoneValid; Static Code -
boolean emailValid = FormUtils.checkEmail(email); boolean phoneValid = FormUtils.checkPhone(phone); String
emailError = emailValid ? null : "Invalid Email"; String phoneError = phoneValid ? null : "Invalid Phone"; boolean loginEnabled = emailValid && phoneValid; Static Code -
boolean emailValid = FormUtils.checkEmail(email); boolean phoneValid = FormUtils.checkPhone(phone); String
emailError = emailValid ? null : "Invalid Email"; String phoneError = phoneValid ? null : "Invalid Phone"; boolean loginEnabled = emailValid && phoneValid; inState() { Observable<String> emailObservable = toObservable(email); Observable<String> phoneObservable = toObservable(phone); Observable<Boolean> emailValid, phoneValid; emailValid = emailObservable.map( email -> FormUtils.checkEmail(email) ); phoneValid = phoneObservable.map( phone -> FormUtils.checkPhone(phone) ); emailError = toField(emailValid.map( emailValid -> emailValid ? null : "Invalid Email"; ); phoneError = toField(phoneValid.map( phoneValid -> phoneValid ? null : "Invalid Phone"; ); loginEnabled = toField(Observable.combineLatest(emailValid, phoneValid, (emailValid, phoneValid) -> emailValid && phoneValid )); Static Code -
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="state" type="com.manaschaudhari.functional_mvvm.ex1.LoginState" /> </data>
<LinearLayout ... <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="state" type="com.manaschaudhari.functional_mvvm.ex1.LoginState" /> </data> <LinearLayout ... -
<EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:error="@{state.phoneError}" android:text="@={state.phone}" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content"
android:enabled="@{state.loginEnabled}" android:text="@string/login" /> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="state" type="com.manaschaudhari.functional_mvvm.ex1.LoginState" /> </data> <LinearLayout ... -
public class LoginActivity extends AppCompatActivity { @Override protected void
onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityLoginBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_login); binding.setState(new LoginState()); } } -
public class LoginActivity extends AppCompatActivity { @Override protected void
onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityLoginBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_login); binding.setState(new LoginState()); } } No “findViewById” No listeners -
Mutation a = 1 c = 10 * a —>
10 a = 3 print(c) —> 10 Imperative Code -
Mutation a = 1 c = 10 * a —>
10 a = 3 print(c) —> 10 Imperative Code Reactive Code -
Mutation a = 1 c = 10 * a —>
10 a = 3 print(c) —> 10 a = 1 c = 10 * a —> 10 a = 3 print(c) —> 30 Imperative Code Reactive Code -
Reactive - How? • Java is not a reactive language
• Reactive behavior using RxJava library -
RxJava • Provides Observable class • Stream of values •
Operators for manipulating streams -
Login Example with RxJava // Inputs Observable<String> email; Observable<String> phone;
// Outputs Observable<String> emailError; Observable<String> phoneError; Observable<Boolean> loginEnabled; -
Observable<Boolean> emailValid = email.map(new Func1<String, Boolean>() { @Override public Boolean
call(String email) { return FormUtils.checkEmail(email); } }) Observable<Boolean> emailValid = email.map(new Func1<String, Boolean>() { @Override public Boolean call(String email) { return FormUtils.checkEmail(email); } }) -
Observable<Boolean> emailValid = email.map(new Func1<String, Boolean>() { @Override public Boolean
call(String email) { return FormUtils.checkEmail(email); } }) -
Observable<Boolean> emailValid = email.map( email -> FormUtils.checkEmail(email); ); Observable<Boolean> emailValid
= email.map(new Func1<String, Boolean>() { @Override public Boolean call(String email) { return FormUtils.checkEmail(email); } }) -
Observable<Boolean> emailValid = email.map( email -> FormUtils.checkEmail(email); ); Observable<Boolean> emailValid
= email.map(new Func1<String, Boolean>() { @Override public Boolean call(String email) { return FormUtils.checkEmail(email); } }) Observable<Boolean> emailValid = email.map( email -> FormUtils.checkEmail(email); ); -
// Transformations Observable<Boolean> emailValid = email.map( email -> FormUtils.checkEmail(email); );
Observable<Boolean> phoneValid = phone.map( phone -> FormUtils.checkPhone(phone); ); -
// Transformations Observable<Boolean> emailValid = email.map( email -> FormUtils.checkEmail(email); );
Observable<Boolean> phoneValid = phone.map( phone -> FormUtils.checkPhone(phone); ); emailError = emailValid.map( emailValid -> emailValid ? null : "Invalid Email" ); phoneError = phoneValid.map( phoneValid -> phoneValid ? null : "Invalid Phone"; ); -
// Transformations Observable<Boolean> emailValid = email.map( email -> FormUtils.checkEmail(email); );
Observable<Boolean> phoneValid = phone.map( phone -> FormUtils.checkPhone(phone); ); emailError = emailValid.map( emailValid -> emailValid ? null : "Invalid Email" ); phoneError = phoneValid.map( phoneValid -> phoneValid ? null : "Invalid Phone"; ); loginEnabled = Observable.combineLatest(emailValid, phoneValid, (emailValid, phoneValid) -> emailValid && phoneValid ); -
Binding to View // Inputs Observable<String> email; // <- emailEditText.text
Observable<String> phone; // <- phoneEditText.text // Outputs Observable<String> emailError; // -> emailEditText.error Observable<String> phoneError; // —> phoneEditText.error Observable<Boolean> loginEnabled; // -> loginButton.enabled -
Observable -> View loginEnabled.subscribe(RxView.enabled(loginButton)); @Override protected void onDestroy() { subscription.unsubscribe();
super.onDestroy(); } RxJava requires cleanup New Boilerplate :/ Subscription subscription = -
Data Binding <layout> <data> <variable name="state" type="com.manaschaudhari.functional_mvvm.LoginState" />
</data> <Button android:enabled=“@{state.loginEnabled}” /> </layout> -
Binding Attributes <Button android:enabled=“@{state.loginEnabled}” /> public class LoginState { public
ObservableField<Boolean> loginEnabled; -
Binding Attributes <Button android:enabled=“@{state.loginEnabled}” /> public class LoginState { public
ObservableField<Boolean> loginEnabled; loginEnabled.set(true) -
Binding Attributes <EditText android:text=“@={state.email}” /> public class LoginState { public
ObservableField<String> email; } User input -
Code Generation // AutoGenerated for activity_login.xml public class ActivityLoginBinding {
public void setState(LoginState state); } <variable name="state" type="com.manaschaudhari.functional_mvvm.LoginState" /> -
rx.Observable databinding.ObservableField View Attribute Data Binding Autogenerated code RxBinding Manual
code for each attribute + Cleanup Boilerplate -
rx.Observable databinding.ObservableField View Attribute Data Binding Autogenerated code RxBinding Manual
code for each attribute + Cleanup Boilerplate Converter -
Converter public static <T> Observable<T> toObservable(ObservableField<T> field) { } public
static <T> ObservableField<T> toField(Observable<T> observable) { } -
Observable -> View <Button android:enabled=“@{state.loginEnabled}” /> Observable<Boolean> loginEnabled = Observable.combineLatest(emailValid,
phoneValid, (emailValid, phoneValid) -> emailValid && phoneValid ); -
ObservableField<Boolean> loginEnabled = toField(Observable.combineLatest(emailValid, phoneValid, (emailValid, phoneValid) -> emailValid &&
phoneValid )); Observable -> View <Button android:enabled=“@{state.loginEnabled}” /> Observable<Boolean> loginEnabled = Observable.combineLatest(emailValid, phoneValid, (emailValid, phoneValid) -> emailValid && phoneValid ); -
ObservableField<Boolean> loginEnabled = toField(Observable.combineLatest(emailValid, phoneValid, (emailValid, phoneValid) -> emailValid &&
phoneValid )); Observable -> View <Button android:enabled=“@{state.loginEnabled}” /> Observable<Boolean> loginEnabled = Observable.combineLatest(emailValid, phoneValid, (emailValid, phoneValid) -> emailValid && phoneValid ); ObservableField<Boolean> loginEnabled = toField(Observable.combineLatest(emailValid, phoneValid, (emailValid, phoneValid) -> emailValid && phoneValid )); -
<EditText android:text=“@={state.email}” /> View -> Observable public class LoginState {
public ObservableField<String> email = new ObservableField<>(""); -
<EditText android:text=“@={state.email}” /> View -> Observable public class LoginState {
public ObservableField<String> email = new ObservableField<>(""); Observable<String> emailObservable = toObservable(email); -
<EditText android:text=“@={state.email}” /> View -> Observable public class LoginState {
public ObservableField<String> email = new ObservableField<>(""); Observable<String> emailObservable = toObservable(email); emailObservable.map(...) -
Summary • Removed findViewById & listeners boilerplate using Data Binding
• databinding.ObservableField <—> rx.Observable • No Subscriptions. Memory leak free code by default -
Pattern • State class for presentation logic • View setup
in XML using State instance • Activity only initializes -
Pattern • State class for presentation logic • View setup
in XML using State instance • Activity only initializes This is MVVM -
MVVM Model ViewModel View • Business Logic • Logic that
will remain same for console app -
MVVM Model ViewModel View • Business Logic • Logic that
will remain same for console app • State of the view • eg: Boolean field for whether button is enabled -
MVVM Model ViewModel View • Business Logic • Logic that
will remain same for console app • State of the view • eg: Boolean field for whether button is enabled • Presentation Logic • eg: Button should be disabled when email is invalid -
MVVM Model ViewModel View • Business Logic • Logic that
will remain same for console app • State of the view • eg: Boolean field for whether button is enabled • Presentation Logic • eg: Button should be disabled when email is invalid • Observes for changes in VM and updates itself • Push values in VM when user inputs -
Dependencies • Model is unaware about ViewModel • ViewModel is
unaware about View • Multiple views can share a same ViewModel Model ViewModel View -
Composition Item Listing Item Details Item Details . . .
Item Checkout Item Details Item Customization -
Composition Item Listing Item Details Item Details . . .
Item Checkout Item Details Item Customization Reuse Plugin ViewModels to build the UI -
Item Checkout Item Details Item Customisation class ItemCheckoutViewModel { ItemViewModel
detailVM; ItemCustomisationViewModel customisationVM; // Initialise in constructor } -
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation=“vertical” > <include layout=“@layout/row_item_details" bind:vm="@{vm.detailVM}"/>
<include layout="@layout/row_item_customisation" bind:vm="@{vm.customisationVM}"/> </LinearLayout> Item Checkout Item Details Item Customisation <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation=“vertical” > <include layout=“@layout/row_item_details" bind:vm="@{vm.detailVM}"/> <include layout="@layout/row_item_customisation" bind:vm="@{vm.customisationVM}"/> </LinearLayout> -
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation=“vertical” > <include layout=“@layout/row_item_details" bind:vm="@{vm.detailVM}"/>
<include layout="@layout/row_item_customisation" bind:vm="@{vm.customisationVM}"/> </LinearLayout> Item Checkout Item Details Item Customisation -
Beauty of MVVM • Consistent View Setup from: • layout_id
• ViewModel object compatible with that layout -
Dynamic Composition • List<ViewModel> • Child layout Item Listing Item
Details Item Details . . -
Dynamic Composition • List<ViewModel> • Child layout Item Listing Item
Details Item Details . . <android.support.v7.widget.RecyclerView bind:items="@{vm.itemViewModels}" bind:item_layout=“@{@layout/row_item}” /> -
Custom Attributes <android.support.v7.widget.RecyclerView bind:items="@{viewModel.itemViewModels}" bind:item_layout=“@{@layout/row_item}” /> @BindingAdapter(value = {"items", "item_layout"})
public static void bindAdapter(RecyclerView recyclerView, List<?> states, @LayoutRes int layoutId) { ... } -
Dependencies • ViewModel cannot have references to android.Context • How
to invoke actions like Navigation? -
Dependencies • ViewModel cannot have references to android.Context • How
to invoke actions like Navigation? Abstract actions into an interface -
public class ItemViewModel { public final Action0 itemClicked; public
ItemViewModel(final Item item, final Navigator navigator) { itemClicked = () -> navigator.openItemDetailsPage(item); } } public interface Navigator { void openItemDetailsPage(Item item); } -
Testability • All UI interactions can be triggered on ViewModels
• All UI states can be asserted from ViewModel • Unit Tests instead of Instrumentation -> Faster -
Testability @Test public void detailsPage_isOpened_onClick() throws Exception { Item
item = new Item("Item 1"); Navigator mockNavigator = mock(Navigator.class); ItemViewModel viewModel = new ItemViewModel(item, mockNavigator); viewModel.itemClicked.call(); verify(mockNavigator).openDetailsPage(item); } -
Conclusions • RxJava and Data Binding together • UI =
express output as a function of input • MVVM • Easy composition of views