Android Data Binding in action using MVVM pattern - droidconUK

  • View
    1.008

  • Download
    1

  • Category

    Mobile

Preview:

Citation preview

#droidconUK

Android Data Binding in action using MVVM pattern

Fabio Collini

droidcon London October 2016

2

Ego slide

@fabioCollini linkedin.com/in/fabiocollini github.com/fabioCollini medium.com/@fabioCollini codingjam.it

3

Agenda

1. Data Binding basics 2. Custom attributes 3. Components 4. Two Way Data Binding 5. Data Binding + RxJava 6. Model View ViewModel

#droidconUK - London - October 2016 - @fabioCollini 4

1Data Binding basics

Google I/O 2016

5

Google I/O 2015

6github.com/fabioCollini/DataBindingInAction

7

match_result.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout style="@style/root_layout" xmlns:android=“http://schemas.android.com/apk/res/android"> <ImageView android:id="@+id/result_gif" style="@style/gif"/> <LinearLayout style="@style/team_layout"> <TextView android:id="@+id/home_team" style="@style/name"/> <TextView android:id="@+id/home_goals" style="@style/goals"/> </LinearLayout> <LinearLayout style="@style/team_layout"> <TextView android:id="@+id/away_team" style="@style/name"/> <TextView android:id="@+id/away_goals" style="@style/goals"/> </LinearLayout> </LinearLayout>

8

public class TeamScore { private final String name; private final int goals;

//constructor and getters}

public class MatchResult { private final TeamScore homeTeam; private final TeamScore awayTeam; private final String gifUrl;

//constructor and getters}

dataBinding { enabled = true }

9

build.gradle

android { //... //... defaultConfig { //...____} buildTypes { //...____}

}

<?xml version="1.0" encoding="utf-8"?> <layout> <LinearLayout style=“@style/root_layout" xmlns:android="http://schemas.android.com/apk/res/android"> <ImageView android:id="@+id/result_gif" style="@style/gif"/> <LinearLayout style="@style/team_layout"> <TextView android:id="@+id/home_team" style="@style/name"/> <TextView android:id="@+id/home_goals" style="@style/goals"/> </LinearLayout> <LinearLayout style="@style/team_layout"> <TextView android:id="@+id/away_team" style="@style/name"/> <TextView android:id="@+id/away_goals" style="@style/goals"/> </LinearLayout> </LinearLayout> </layout>

10

Data Binding layout

<LinearLayout style="@style/root_layout" xmlns:android=“http://schemas.android.com/apk/res/android"> <ImageView android:id="@+id/result_gif" style="@style/gif"/> <LinearLayout style="@style/team_layout"> <TextView android:id="@+id/home_team" style="@style/name"/> <TextView android:id="@+id/home_goals" style="@style/goals"/> </LinearLayout> <LinearLayout style="@style/team_layout"> <TextView android:id="@+id/away_team" style="@style/name"/> <TextView android:id="@+id/away_goals" style="@style/goals"/> </LinearLayout> </LinearLayout>

<?xml version="1.0" encoding="utf-8"?><layout>

</layout>

<LinearLayout style="@style/root_layout" xmlns:android=“http://schemas.android.com/apk/res/android"> <ImageView android:id="@+id/result_gif" style="@style/gif"/> <LinearLayout style="@style/team_layout"> <TextView android:id="@+id/home_team" style="@style/name"/> <TextView android:id="@+id/home_goals" style="@style/goals"/> </LinearLayout> <LinearLayout style="@style/team_layout"> <TextView android:id="@+id/away_team" style="@style/name"/> <TextView android:id="@+id/away_goals" style="@style/goals"/> </LinearLayout> </LinearLayout>

11

One layout traversal

mat

ch_r

esul

t.xm

lM

atch

Resu

ltBin

ding

.java Auto generated class

<?xml version="1.0" encoding="utf-8"?><layout>

</layout>

public class MatchResultBinding extends android.databinding.ViewDataBinding { // ... public final android.widget.ImageView resultGif; public final android.widget.TextView homeTeam; public final android.widget.TextView homeGoals; public final android.widget.TextView awayTeam; public final android.widget.TextView awayGoals; // ...}

12

public class MatchResultActivity extends AppCompatActivity { private MatchResultBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.match_result); MatchResult result = getIntent().getParcelableExtra("RESULT"); if (result.getHomeTeam() != null) { binding.homeTeam.setText(result.getHomeTeam().getName()); binding.homeGoals.setText( Integer.toString(result.getHomeTeam().getGoals())); }if1 if (result.getAwayTeam() != null) { binding.awayTeam.setText(result.getAwayTeam().getName()); binding.awayGoals.setText( Integer.toString(result.getAwayTeam().getGoals())); }if Glide.with(this).load(result.getGifUrl()) .placeholder(R.drawable.loading).into(binding.resultGif); }onCreate}activity

13

Variable in layout<?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://schemas.android.com/apk/res/android">

Automatic null check

<data> <variable name="result" type="it.droidcon.databinding.MatchResult"/> </data> <LinearLayout style="@style/root_layout"> <ImageView android:id="@+id/result_gif" style="@style/gif"/> <LinearLayout style="@style/team_layout"> <TextView style=“@style/name" android:text="@{result.homeTeam.name}"/> <TextView style="@style/goals" android:text="@{Integer.toString(result.homeTeam.goals)}"/> </LinearLayout> <LinearLayout style="@style/team_layout"> <TextView style="@style/name" android:text="@{result.awayTeam.name}"/> <TextView style="@style/goals" android:text="@{Integer.toString(result.awayTeam.goals)}"/> </LinearLayout> </LinearLayout> </layout>

14

public class MatchResultActivity extends AppCompatActivity { private MatchResultBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.match_result); MatchResult result = getIntent().getParcelableExtra("RESULT");

binding.setResult(result);if Glide.with(this).load(result.getGifUrl()) .placeholder(R.drawable.loading).into(binding.resultGif); }onCreate}activity

15

Code in XML? Are you serious?!?

16

Complex code in XML is

NOT a best practice

#droidconUK - London - October 2016 - @fabioCollini 17

2Custom attributes

18

@BindingAdapter

<ImageView android:id="@+id/result_gif" style="@style/gif"/>

Glide.with(this).load(result.getGifUrl()) .placeholder(R.drawable.loading).into(binding.resultGif);

<ImageView style="@style/gif" app:imageUrl="@{result.gifUrl}"/>

@BindingAdapter("imageUrl") public static void loadImage(ImageView view, String url) { Glide.with(view.getContext()).load(url) .placeholder(R.drawable.loading).into(view);}

19

public class MatchResultActivity extends AppCompatActivity { private MatchResultBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView( this, R.layout.match_result); MatchResult result = getIntent().getParcelableExtra("RESULT");

binding.setResult(result); }onCreate}activity

Annotated methods are static but…

@BindingAdapter("something") public static void bindSomething(View view, AnyObject b) { MyBinding binding = DataBindingUtil.findBinding(view); MyObject myObject = binding.getMyObject(); //… TextView myTextView = binding.myTextView; //… }

Can be any object

Get the layout binding

Get the connected objects

Access to all the views

Can be defined anywhere Can be used everywhereCan be any View

@BindingAdapter("goals") public static void bindGoals(TextView view, int goals) { view.setText(Integer.toString(goals));}__

21

BindingAdapter

<TextView style="@style/goals" android:text="@{Integer.toString(result.awayTeam.goals)}"/>

<TextView style="@style/goals" app:goals="@{result.awayTeam.goals}"/>

#droidconUK - London - October 2016 - @fabioCollini 22

3Components

23

<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="result" type="it.droidcon.databinding.MatchResult"/> </data> <LinearLayout style="@style/root_layout"> <ImageView style="@style/gif" app:imageUrl="@{result.gifUrl}"/>

<LinearLayout style="@style/team_layout"> <TextView style="@style/name" android:text="@{result.awayTeam.name}"/> <TextView style="@style/goals" app:goals="@{result.awayTeam.goals}"/> </LinearLayout> </LinearLayout> </layout>

<LinearLayout style="@style/team_layout"> <TextView style="@style/name" android:text="@{result.homeTeam.name}"/> <TextView style="@style/goals" app:goals="@{result.homeTeam.goals}"/> </LinearLayout>

24

<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">

</layout>

team_detail.xml

<data> <variable name="team" type="it.droidcon.databinding.TeamScore"/> </data>

<LinearLayout style="@style/team_layout"> <TextView style="@style/name" android:text="@{team.name}"/> <TextView style="@style/goals" app:goals="@{team.goals}"/> </LinearLayout>

</data> <LinearLayout style="@style/root_layout"> <ImageView style="@style/gif" app:imageUrl="@{result.gifUrl}"/>

<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:bind="http://schemas.android.com/tools"> <data> <variable name="result" type="it.droidcon.databinding.MatchResult"/>

<include layout="@layout/team_detail" /> <include layout="@layout/team_detail" /> </LinearLayout> </layout>

bind:team="@{result.homeTeam}"

bind:team="@{result.awayTeam}"

#droidconUK - London - October 2016 - @fabioCollini 26

4Two Way Data Binding

27

28

public class QuestionInfo { public String answer = ""; public int countdown = 10;

public int decrementCountdown() { return --countdown; }_ }_

29

public class QuestionInfo { public String answer = ""; public int countdown = 10;

public int decrementCountdown() { return --countdown; }_ }_

<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="info" type="it.droidcon.databinding.question.QuestionInfo"/> </data> <LinearLayout style="@style/form_root"> <TextView style="@style/question"/> <EditText style="@style/answer" android:text="@{info.answer}" /> <Button style="@style/form_button" android:enabled="@{info.countdown > 0 &amp;&amp; !info.answer.empty}"/> <TextView style="@style/countdown" android:text="@{Integer.toString(info.countdown)}" /> </LinearLayout> </layout>

public class QuestionActivity extends AppCompatActivity { private QuestionInfo info; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); QuestionBinding binding = DataBindingUtil.setContentView(this, R.layout.question); info = new QuestionInfo(); binding.setInfo(info); Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { int newValue = info.decrementCountdown(); if (newValue > 0) { handler.postDelayed(this, 1000); } } }, 1000); } } 30

public class QuestionInfo { public String answer = ""; public int countdown = 10;

public int decrementCountdown() { return --countdown; }_ }_

31

32

Views are not automatically updated :(

package android.databinding;public interface Observable { void addOnPropertyChangedCallback( OnPropertyChangedCallback callback); void removeOnPropertyChangedCallback( OnPropertyChangedCallback callback); abstract class OnPropertyChangedCallback { public abstract void onPropertyChanged( Observable sender, int propertyId); }}

33

Observable hierarchy

34

public class QuestionInfo extends BaseObservable {_ private String answer = ""; private int countdown = 10; public int decrementCountdown() { --countdown; notifyPropertyChanged(BR.countdown); return countdown; }__ @Bindable public String getAnswer() { return answer; }getAnswer public void setAnswer(String answer) { this.answer = answer; notifyPropertyChanged(BR.answer); }setAnswer @Bindable public int getCountdown() { return countdown; }getCountdown}___

35

public class QuestionInfo {_ public final ObservableField<String> answer = new ObservableField<>(""); public final ObservableInt countdown = new ObservableInt(10); public int decrementCountdown() { int value = countdown.get() - 1; countdown.set(value); return value; }__ }___

<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="info" type="it.droidcon.databinding.question.QuestionInfo"/> </data> <LinearLayout style="@style/form_root"> <TextView style="@style/question"/> <EditText style="@style/answer" android:text="@{info.answer}" /> <Button style="@style/form_button" android:enabled="@{info.countdown > 0 &amp;&amp; !info.answer.empty}"/> <TextView style="@style/countdown" android:text="@{Integer.toString(info.countdown)}" /> </LinearLayout> </layout>

36

ObservableField<String>

ObservableInt

ObservableInt

public class QuestionInfo {_ public final ObservableField<String> answer = new ObservableField<>(""); public final ObservableInt countdown = new ObservableInt(10); public int decrementCountdown() { int value = countdown.get() - 1; countdown.set(value); return value; }__ }___

ObservableField<String>

37

38

Two way Data Binding@BindingAdapter("binding") public static void bindEditText(EditText view, final ObservableString observable) { Pair<ObservableString, TextWatcherAdapter> pair = (Pair) view.getTag(R.id.bound_observable); if (pair == null || pair.first != observable) { if (pair != null) view.removeTextChangedListener(pair.second); TextWatcherAdapter watcher = new TextWatcherAdapter() { @Override public void onTextChanged(CharSequence s, int a, int b, int c) { observable.set(s.toString()); } }; view.setTag(R.id.bound_observable, new Pair<>(observable, watcher)); view.addTextChangedListener(watcher); } String newValue = observable.get(); if (!view.getText().toString().equals(newValue)) view.setText(newValue);}

medium.com/@fabioCollini/android-data-binding-f9f9d3afc761

39

<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="info" type="it.droidcon.databinding.question.QuestionInfo"/> </data> <LinearLayout style="@style/form_root"> <TextView style="@style/question"/> <EditText style="@style/answer" android:text="@={info.answer}" /> <Button style="@style/form_button" android:enabled="@{info.countdown > 0 &amp;&amp; !info.answer.empty}"/> <TextView style="@style/countdown" android:text="@{Integer.toString(info.countdown)}" /> </LinearLayout> </layout>

40

Two way data binding

41

42

43

Layout

QuestionInfo

Binding

Text

Wat

cher

set(…

)addOnProperty

ChangedCallbackset(…)

if (changed)

WeakReference

if (changed)

#droidconUK - London - October 2016 - @fabioCollini 44

5Data Binding + RxJava

45

<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="info" type="it.droidcon.databinding.question.QuestionInfo"/> </data> <LinearLayout style="@style/form_root"> <TextView style="@style/question"/> <EditText style="@style/answer" android:text="@={info.answer}" /> <Button style="@style/form_button" android:enabled="@{info.countdown > 0 &amp;&amp; !info.answer.empty}"/> <TextView style="@style/countdown" android:text="@{Integer.toString(info.countdown)}" /> </LinearLayout> </layout>

public class QuestionInfo extends BaseObservable {_ private String answer = ""; private int countdown = 10; public int decrementCountdown() { --countdown; notifyPropertyChanged(BR.countdown); notifyPropertyChanged(BR.sendEnabled); return countdown; }__ @Bindable public String getAnswer() { return answer; }getAnswer public void setAnswer(String answer) { this.answer = answer; notifyPropertyChanged(BR.answer); notifyPropertyChanged(BR.sendEnabled); }setAnswer @Bindable public int getCountdown() { return countdown; }getCountdown

@Bindable public boolean isSendEnabled() { return !answer.isEmpty() && countdown > 0; }isButtonEnabled}___

47

Not an Observable, View is not updated!

public class QuestionInfo {_ public final ObservableField<String> answer = new ObservableField<>(""); public final ObservableInt countdown = new ObservableInt(10); public int decrementCountdown() { int value = countdown.get() - 1; countdown.set(value); return value; }__ public boolean isSendEnabled() { return !answer.get().isEmpty() && countdown.get() > 0; } }___

48

RxJava FTW!

ObservableField<T> rx.Observable<T>

50

ObservableField<T> rx.Observable<T>

public static <T> rx.Observable<T> toRx(ObservableField<T> observableField) { return rx.Observable.fromEmitter(emitter -> { emitter.onNext(observableField.get()); OnPropertyChangedCallback callback = new OnPropertyChangedCallback() { @Override public void onPropertyChanged(Observable observable, int i) { emitter.onNext(((ObservableField<T>) observable).get()); } }; observableField.addOnPropertyChangedCallback(callback); emitter.setCancellation(() -> observableField.removeOnPropertyChangedCallback(callback)); }, Emitter.BackpressureMode.BUFFER);}

51

public class QuestionInfo {_ public final ObservableField<String> answer = new ObservableField<>(""); public final ObservableInt countdown = new ObservableInt(10);

public final ObservableBoolean sendEnabled = new ObservableBoolean(); public int decrementCountdown() { int value = countdown.get() - 1; countdown.set(value); return value; }__ }___

52

public class QuestionActivity extends AppCompatActivity { private QuestionInfo info; private Subscription subscription; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { //... } @Override protected void onStart() { super.onStart(); subscription = Observable.combineLatest( toRx(info.answer), toRx(info.countdown), (answer, countdown) -> !answer.isEmpty() && countdown > 0 ).subscribe(info.sendEnabled::set); } @Override protected void onStop() { super.onStop(); subscription.unsubscribe(); }}

53

compile 'com.cantrowitz:rxbroadcast:1.0.0'

public class ConnectionChecker { private Context context; public ConnectionChecker(Context context) { this.context = context; } public Observable<Boolean> getConnectionStatus() { IntentFilter filter = new IntentFilter( ConnectivityManager.CONNECTIVITY_ACTION); return RxBroadcast.fromBroadcast(context, filter) .map(i -> getNetworkInfo()) .map(info -> info != null && info.isConnected()) .distinctUntilChanged(); } private NetworkInfo getNetworkInfo() { ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); return connectivityManager.getActiveNetworkInfo(); }}

public class QuestionActivity extends AppCompatActivity { private QuestionInfo info; private Subscription subscription; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { //... }onCreate @Override protected void onStart() { super.onStart(); subscription = Observable.combineLatest( toRx(info.answer), toRx(info.countdown), connectionChecker.getConnectionStatus(), (answer, countdown, connected) -> !answer.isEmpty() && countdown > 0 && connected ).subscribe(info.sendEnabled::set); }onStart @Override protected void onStop() { super.onStop(); subscription.unsubscribe(); }onStop}_

54

#droidconUK - London - October 2016 - @fabioCollini 55

6MVVM

56

MatchResultViewModel

public class MatchResultViewModel { public final ObservableField<MatchResult> result = new ObservableField<>(); public final ObservableBoolean loading = new ObservableBoolean(); public void reload() { loading.set(true); reloadInBackground(result -> { loading.set(false); this.result.set(result); }); } }

<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:bind="http://schemas.android.com/tools"> <data> <variable name="viewModel" type="it.droidcon.databinding.MatchResultViewModel"/> </data> <LinearLayout style="@style/root_layout"> <ImageView style="@style/gif" app:imageUrl="@{viewModel.result.gifUrl}"/>

<include layout="@layout/team_detail" /> <include layout="@layout/team_detail" /> </LinearLayout> </layout>

bind:team="@{viewModel.result.homeTeam}"

bind:team="@{viewModel.result.awayTeam}"

ObservableField

58

Visibility

<FrameLayout style="@style/progress_layout" android:visibility= "@{viewModel.loading ? View.VISIBLE : View.GONE}"> <ProgressBar style="@style/progress"/></FrameLayout>

<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:bind="http://schemas.android.com/tools"> <data> <import type="android.view.View"/> <variable name="viewModel" type="it.droidcon.databinding.MatchResultViewModel"/> </data> <FrameLayout style="@style/main_container"> <LinearLayout style="@style/root_layout"> <!-- ... --> </LinearLayout>

</FrameLayout> </layout>

59

Visibility

<FrameLayout style="@style/progress_layout" app:visibleOrGone="@{viewModel.loading}"> <ProgressBar style="@style/progress"/></FrameLayout>

@BindingAdapter("visibleOrGone") public static void bindVisibleOrGone(View view, boolean b) { view.setVisibility(b ? View.VISIBLE : View.GONE); }____

@BindingAdapter("visible") public static void bindVisible(View view, boolean b) { view.setVisibility(b ? View.VISIBLE : View.INVISIBLE); }

<LinearLayout style="@style/root_layout" android:onClick="@{???}"> <!-- ... --></LinearLayout>

60

}___

public class MatchResultViewModel {

public final ObservableField<MatchResult> result = new ObservableField<>();

public final ObservableBoolean loading = new ObservableBoolean();

public void reload() { loading.set(true); reloadInBackground(result -> { loading.set(false); this.result.set(result); }); }__

<LinearLayout style="@style/root_layout" android:onClick="@{v -> viewModel.reload()}"> <!-- ... --></LinearLayout>

61

public void reload() { //.. }__

<LinearLayout style="@style/root_layout" android:onClick=“@{viewModel::reload}”> <!-- ... --></LinearLayout>

public void reload(View v) { //.. }__

@BindingAdapter("android:onClick") public static void bindOnClick(View view, final Runnable listener) { view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { listener.run(); }____ });}___

<LinearLayout style="@style/root_layout" android:onClick=“@{viewModel::reload}”> <!-- ... --></LinearLayout>

public void reload() { //.. }__

62

Final layout<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:bind="http://schemas.android.com/tools"> <data> <!-- ... --> </data> <FrameLayout style="@style/main_container"> <LinearLayout style="@style/root_layout" android:onClick=“@{viewModel::reload}”> <!-- ... --> </LinearLayout> <FrameLayout style="@style/progress_layout" app:visibleOrGone="@{viewModel.loading}"> <ProgressBar style="@style/progress"/> </FrameLayout> </FrameLayout> </layout>

63

Model View ViewModel

View

ViewModel

Model

DataBinding

Retained on configuration change

Saved in Activity or Fragment state

Activity or Fragment

64

MVVM

View

ViewModel

Model

DataBinding

View

Presenter

Model

MVPVs

MVVM MVPVs

Less Java code

if (view != null)

A/B testing on View

Sometimes we need an Activity :(

Testable code Testable code

Less XML

No more

66

github.com/fabioCollini/LifeCycleBinder

Move your Android code to testable Java classes

Custom attributes Reusable UI code

67

Data binding

Includes UI componentsRxJava Easy composition

68

Linksdeveloper.android.com/tools/data-binding/guide.html

Google I/O 2015 - What's new in Android Data Binding -- Write Apps Faster (Android Dev Summit 2015) Advanced Data Binding - Google I/O 2016

George Mount medium profile

Radosław Piekarz: RxJava meets Android Data Binding

Florina Muntenescu: A Journey Through MV Wonderland Bill Phillips: Shades of MVVM

69

Thanks for your attention!

Questions?

This presentation will be soon available on the droidcon London website at

uk.droidcon.com/#skillscasts