Upload
fabio-collini
View
1.005
Download
1
Embed Size (px)
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 && !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 && !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 && !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 && !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