Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Functional MVVM using RxJava and Data Binding

Functional MVVM using RxJava and Data Binding

Manas Chaudhari

November 10, 2016
Tweet

Other Decks in Programming

Transcript

  1. 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?
  2. 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
 }
  3. void checkEmail(String email) {
 boolean emailValid = FormUtils.checkEmail(email);
 String emailError

    = emailValid ? null : "Invalid Email";
 emailEditText.setError(emailError);
 }
  4. 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);
 }
  5. 
 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;

  6. 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);
 }
  7. 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
  8. 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
  9. 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
 ));
 }
  10. 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
 ));
 }
  11. 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
  12. 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
  13. 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
  14. <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 ...
  15. <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 ...
  16. 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());
 }
 
 }
  17. 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
  18. Mutation a = 1 c = 10 * a —>

    10 a = 3 print(c) —> 10 Imperative Code
  19. Mutation a = 1 c = 10 * a —>

    10 a = 3 print(c) —> 10 Imperative Code Reactive Code
  20. 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
  21. Reactive - How? • Java is not a reactive language

    • Reactive behavior using RxJava library
  22. Map

  23. Login Example with RxJava // Inputs
 Observable<String> email;
 Observable<String> phone;


    // Outputs
 Observable<String> emailError;
 Observable<String> phoneError;
 Observable<Boolean> loginEnabled;
  24. 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);
 }
 })
  25. Observable<Boolean> emailValid = email.map(new Func1<String, Boolean>() {
 @Override
 public Boolean

    call(String email) {
 return FormUtils.checkEmail(email);
 }
 })
  26. 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);
 }
 })
  27. 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);
 );
  28. // Transformations
 Observable<Boolean> emailValid = email.map(
 email -> FormUtils.checkEmail(email);
 );

    Observable<Boolean> phoneValid = phone.map(
 phone -> FormUtils.checkPhone(phone);
 );
  29. // 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";
 );
  30. // 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
 );
  31. 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
  32. Code Generation // AutoGenerated for activity_login.xml
 public class ActivityLoginBinding {


    
 public void setState(LoginState state);
 } <variable
 name="state"
 type="com.manaschaudhari.functional_mvvm.LoginState" />

  33. Converter public static <T> Observable<T> toObservable(ObservableField<T> field) { }
 public

    static <T> ObservableField<T> toField(Observable<T> observable) { }
  34. 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
 );
  35. 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
 ));
  36. <EditText
 android:text=“@={state.email}” /> View -> Observable public class LoginState {

    public ObservableField<String> email = new ObservableField<>("");
  37. <EditText
 android:text=“@={state.email}” /> View -> Observable public class LoginState {

    public ObservableField<String> email = new ObservableField<>(""); Observable<String> emailObservable = toObservable(email);
  38. <EditText
 android:text=“@={state.email}” /> View -> Observable public class LoginState {

    public ObservableField<String> email = new ObservableField<>(""); Observable<String> emailObservable = toObservable(email); emailObservable.map(...)
  39. Summary • Removed findViewById & listeners boilerplate using Data Binding

    • databinding.ObservableField <—> rx.Observable • No Subscriptions. Memory leak free code by default
  40. Pattern • State class for presentation logic • View setup

    in XML using State instance • Activity only initializes
  41. Pattern • State class for presentation logic • View setup

    in XML using State instance • Activity only initializes This is MVVM
  42. 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
  43. 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
  44. 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
  45. Dependencies • Model is unaware about ViewModel • ViewModel is

    unaware about View • Multiple views can share a same ViewModel Model ViewModel View
  46. Composition Item Listing Item Details Item Details .
 .
 .

    Item Checkout Item Details Item Customization
  47. Composition Item Listing Item Details Item Details .
 .
 .

    Item Checkout Item Details Item Customization Reuse Plugin ViewModels to build the UI
  48. Item Checkout Item Details Item Customisation class ItemCheckoutViewModel {
 ItemViewModel

    detailVM;
 ItemCustomisationViewModel customisationVM;
 
 // Initialise in constructor
 
 }
  49. <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>
  50. <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
  51. Beauty of MVVM • Consistent View Setup from: • layout_id

    • ViewModel object compatible with that layout
  52. 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}” />
  53. Dependencies • ViewModel cannot have references to android.Context • How

    to invoke actions like Navigation? Abstract actions into an interface
  54. 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);
 }
  55. Testability • All UI interactions can be triggered on ViewModels

    • All UI states can be asserted from ViewModel • Unit Tests instead of Instrumentation -> Faster
  56. 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);
 }

  57. Conclusions • RxJava and Data Binding together • UI =

    express output as a function of input • MVVM • Easy composition of views