Slide 1

Slide 1 text

Functional MVVM using RxJava and Data Binding Manas Chaudhari

Slide 2

Slide 2 text

User interfaces • Lot of changing states • Data from server • Change on user action

Slide 3

Slide 3 text

Login Example

Slide 4

Slide 4 text

Login Example Validations

Slide 5

Slide 5 text

Login Example Validations

Slide 6

Slide 6 text

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?

Slide 7

Slide 7 text

Need Listeners String email = emailEditText.getText();

Slide 8

Slide 8 text

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
 }

Slide 9

Slide 9 text

void checkEmail(String email) {
 boolean emailValid = FormUtils.checkEmail(email);
 String emailError = emailValid ? null : "Invalid Email";
 emailEditText.setError(emailError);
 }

Slide 10

Slide 10 text

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);
 }

Slide 11

Slide 11 text


 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;


Slide 12

Slide 12 text

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);
 }

Slide 13

Slide 13 text

Problems • Mutation -> refresh chain • Boilerplate code for connecting data to view

Slide 14

Slide 14 text

public class LoginState {
 // Inputs
 public final ObservableField email;
 public final ObservableField phone;
 
 // Outputs
 public final ReadOnlyField emailError;
 public final ReadOnlyField phoneError;
 public final ReadOnlyField loginEnabled; public class LoginState {
 // Inputs
 public final ObservableField email;
 public final ObservableField phone;
 
 // Outputs
 public final ReadOnlyField emailError;
 public final ReadOnlyField phoneError;
 public final ReadOnlyField loginEnabled; Solution Preview

Slide 15

Slide 15 text

public class LoginState {
 // Inputs
 public final ObservableField email;
 public final ObservableField phone;
 
 // Outputs
 public final ReadOnlyField emailError;
 public final ReadOnlyField phoneError;
 public final ReadOnlyField loginEnabled; Solution Preview Input, Output Fields

Slide 16

Slide 16 text

LoginState() {
 
 Observable emailObservable = toObservable(email);
 Observable phoneObservable = toObservable(phone);
 
 Observable 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 emailObservable = toObservable(email);
 Observable phoneObservable = toObservable(phone);
 
 Observable 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
 ));
 }

Slide 17

Slide 17 text

LoginState() {
 
 Observable emailObservable = toObservable(email);
 Observable phoneObservable = toObservable(phone);
 
 Observable 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
 ));
 }

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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 emailObservable = toObservable(email);
 Observable phoneObservable = toObservable(phone);
 Observable 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

Slide 21

Slide 21 text


 
 
 
 
 
 
 
 
 
 
 
 
 


Slide 22

Slide 22 text


 
 
 
 
 
 
 
 


Slide 23

Slide 23 text

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());
 }
 
 }

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Mutation Imperative Code

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Reactive - How? • Java is not a reactive language • Reactive behavior using RxJava library

Slide 30

Slide 30 text

RxJava • Provides Observable class • Stream of values • Operators for manipulating streams

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

Map

Slide 33

Slide 33 text

Combine Latest

Slide 34

Slide 34 text

Login Example with RxJava // Inputs
 Observable email;
 Observable phone;
 // Outputs
 Observable emailError;
 Observable phoneError;
 Observable loginEnabled;

Slide 35

Slide 35 text

Observable emailValid = email.map(new Func1() {
 @Override
 public Boolean call(String email) {
 return FormUtils.checkEmail(email);
 }
 }) Observable emailValid = email.map(new Func1() {
 @Override
 public Boolean call(String email) {
 return FormUtils.checkEmail(email);
 }
 })

Slide 36

Slide 36 text

Observable emailValid = email.map(new Func1() {
 @Override
 public Boolean call(String email) {
 return FormUtils.checkEmail(email);
 }
 })

Slide 37

Slide 37 text

Observable emailValid = email.map(
 email -> FormUtils.checkEmail(email);
 ); Observable emailValid = email.map(new Func1() {
 @Override
 public Boolean call(String email) {
 return FormUtils.checkEmail(email);
 }
 })

Slide 38

Slide 38 text

Observable emailValid = email.map(
 email -> FormUtils.checkEmail(email);
 ); Observable emailValid = email.map(new Func1() {
 @Override
 public Boolean call(String email) {
 return FormUtils.checkEmail(email);
 }
 }) Observable emailValid = email.map(
 email -> FormUtils.checkEmail(email);
 );

Slide 39

Slide 39 text

// Transformations
 Observable emailValid = email.map(
 email -> FormUtils.checkEmail(email);
 );

Slide 40

Slide 40 text

// Transformations
 Observable emailValid = email.map(
 email -> FormUtils.checkEmail(email);
 ); Observable phoneValid = phone.map(
 phone -> FormUtils.checkPhone(phone);
 );

Slide 41

Slide 41 text

// Transformations
 Observable emailValid = email.map(
 email -> FormUtils.checkEmail(email);
 ); Observable phoneValid = phone.map(
 phone -> FormUtils.checkPhone(phone);
 ); emailError = emailValid.map(
 emailValid -> emailValid ? null : "Invalid Email"
 ); 
 phoneError = phoneValid.map(
 phoneValid -> phoneValid ? null : "Invalid Phone";
 );

Slide 42

Slide 42 text

// Transformations
 Observable emailValid = email.map(
 email -> FormUtils.checkEmail(email);
 ); Observable 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
 );

Slide 43

Slide 43 text

Binding to View // Inputs
 Observable email; // <- emailEditText.text
 Observable phone; // <- phoneEditText.text
 // Outputs
 Observable emailError; // -> emailEditText.error
 Observable phoneError; // —> phoneEditText.error
 Observable loginEnabled; // -> loginButton.enabled

Slide 44

Slide 44 text

View -> Observable // Inputs
 Observable email = RxTextView.textChanges(emailEditText);


Slide 45

Slide 45 text

Observable -> View loginEnabled.subscribe(RxView.enabled(loginButton));

Slide 46

Slide 46 text

Observable -> View loginEnabled.subscribe(RxView.enabled(loginButton)); @Override
 protected void onDestroy() {
 subscription.unsubscribe();
 super.onDestroy();
 } RxJava requires cleanup New Boilerplate :/ Subscription subscription =

Slide 47

Slide 47 text

Data Binding 
 
 
 
 
 
 


Slide 48

Slide 48 text

Binding Attributes public class LoginState {
 public ObservableField loginEnabled;

Slide 49

Slide 49 text

Binding Attributes public class LoginState {
 public ObservableField loginEnabled; loginEnabled.set(true)

Slide 50

Slide 50 text

Binding Attributes public class LoginState {
 public ObservableField email; } User input

Slide 51

Slide 51 text

Code Generation // AutoGenerated for activity_login.xml
 public class ActivityLoginBinding {
 
 public void setState(LoginState state);
 } 


Slide 52

Slide 52 text

View Setup ActivityLoginBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_login);
 
 binding.setState(new LoginState());


Slide 53

Slide 53 text

databinding.ObservableField View Attribute Data Binding

Slide 54

Slide 54 text

databinding.ObservableField View Attribute Data Binding Autogenerated code

Slide 55

Slide 55 text

rx.Observable databinding.ObservableField View Attribute Data Binding Autogenerated code RxBinding

Slide 56

Slide 56 text

rx.Observable databinding.ObservableField View Attribute Data Binding Autogenerated code RxBinding Manual code for each attribute
 + Cleanup Boilerplate

Slide 57

Slide 57 text

rx.Observable databinding.ObservableField View Attribute Data Binding Autogenerated code RxBinding Manual code for each attribute
 + Cleanup Boilerplate Converter

Slide 58

Slide 58 text

rx.Observable databinding.ObservableField View Attribute Data Binding Autogenerated code Converter

Slide 59

Slide 59 text

Converter public static Observable toObservable(ObservableField field) { }
 public static ObservableField toField(Observable observable) { }

Slide 60

Slide 60 text

Observable -> View Observable loginEnabled = Observable.combineLatest(emailValid, phoneValid,
 (emailValid, phoneValid) -> emailValid && phoneValid
 );

Slide 61

Slide 61 text

Observable -> View Observable loginEnabled = Observable.combineLatest(emailValid, phoneValid,
 (emailValid, phoneValid) -> emailValid && phoneValid
 );

Slide 62

Slide 62 text

ObservableField loginEnabled = toField(Observable.combineLatest(emailValid, phoneValid,
 (emailValid, phoneValid) -> emailValid && phoneValid
 )); Observable -> View Observable loginEnabled = Observable.combineLatest(emailValid, phoneValid,
 (emailValid, phoneValid) -> emailValid && phoneValid
 );

Slide 63

Slide 63 text

ObservableField loginEnabled = toField(Observable.combineLatest(emailValid, phoneValid,
 (emailValid, phoneValid) -> emailValid && phoneValid
 )); Observable -> View Observable loginEnabled = Observable.combineLatest(emailValid, phoneValid,
 (emailValid, phoneValid) -> emailValid && phoneValid
 ); ObservableField loginEnabled = toField(Observable.combineLatest(emailValid, phoneValid,
 (emailValid, phoneValid) -> emailValid && phoneValid
 ));

Slide 64

Slide 64 text

View -> Observable public class LoginState { public ObservableField email = new ObservableField<>("");

Slide 65

Slide 65 text

View -> Observable public class LoginState { public ObservableField email = new ObservableField<>(""); Observable emailObservable = toObservable(email);

Slide 66

Slide 66 text

View -> Observable public class LoginState { public ObservableField email = new ObservableField<>(""); Observable emailObservable = toObservable(email); emailObservable.map(...)

Slide 67

Slide 67 text

Summary • Removed findViewById & listeners boilerplate using Data Binding • databinding.ObservableField <—> rx.Observable • No Subscriptions. Memory leak free code by default

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

Pattern • State class for presentation logic • View setup in XML using State instance • Activity only initializes This is MVVM

Slide 70

Slide 70 text

MVVM Model ViewModel View

Slide 71

Slide 71 text

MVVM Model ViewModel View • Business Logic • Logic that will remain same for console app

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

Dependencies Model ViewModel View

Slide 76

Slide 76 text

Dependencies • Model is unaware about ViewModel • ViewModel is unaware about View • Multiple views can share a same ViewModel Model ViewModel View

Slide 77

Slide 77 text

LoginState -> ViewModel

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

Composition Item Listing Item Details Item Details .
 .
 . Item Checkout Item Details Item Customization Reuse Plugin ViewModels to build the UI

Slide 80

Slide 80 text

Item Checkout Item Details Item Customisation

Slide 81

Slide 81 text

Item Checkout Item Details Item Customisation class ItemCheckoutViewModel {
 ItemViewModel detailVM;
 ItemCustomisationViewModel customisationVM;
 
 // Initialise in constructor
 
 }

Slide 82

Slide 82 text


 
 
 
 
 
 Item Checkout Item Details Item Customisation 
 
 
 
 
 


Slide 83

Slide 83 text


 
 
 
 
 
 Item Checkout Item Details Item Customisation

Slide 84

Slide 84 text

Beauty of MVVM • Consistent View Setup from: • layout_id • ViewModel object compatible with that layout

Slide 85

Slide 85 text

Dynamic Composition • List • Child layout Item Listing Item Details Item Details .
 .


Slide 86

Slide 86 text

Dynamic Composition • List • Child layout Item Listing Item Details Item Details .
 .


Slide 87

Slide 87 text

Custom Attributes

Slide 88

Slide 88 text

Custom Attributes @BindingAdapter(value = {"items", "item_layout"})
 public static void bindAdapter(RecyclerView recyclerView,
 List states,
 @LayoutRes int layoutId) {
 ...
 }


Slide 89

Slide 89 text

Consistent Api

Slide 90

Slide 90 text

Dependencies • ViewModel cannot have references to android.Context • How to invoke actions like Navigation?

Slide 91

Slide 91 text

Dependencies • ViewModel cannot have references to android.Context • How to invoke actions like Navigation? Abstract actions into an interface

Slide 92

Slide 92 text

public interface Navigator {
 void openItemDetailsPage(Item item);
 }

Slide 93

Slide 93 text

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);
 }

Slide 94

Slide 94 text

Testability • All UI interactions can be triggered on ViewModels • All UI states can be asserted from ViewModel • Unit Tests instead of Instrumentation -> Faster

Slide 95

Slide 95 text

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);
 }


Slide 96

Slide 96 text

Conclusions • RxJava and Data Binding together • UI = express output as a function of input • MVVM • Easy composition of views

Slide 97

Slide 97 text

manas-chaudhari/android-mvvm @chaudharimanas