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

Avatar for Manas Chaudhari

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