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

Droidcon Vietnam 2017: @{app.plaid}

Droidcon Vietnam 2017: @{app.plaid}

A talk on exploring data binding through Plaid

Zarah Dominguez

April 15, 2017
Tweet

Other Decks in Programming

Transcript

  1. Agenda ◉ But another dependency… ◉ Isn’t it just Butterknife?

    ◉ What if I’m working with an existing codebase? All these and more!
  2. Butterknife != Data binding ◉ Simplify findViewById ◉ UI modifications

    from POJOs ◉ Mathematical and logical operations ◉ Null protection affordance ◉ Imports and variables
  3. There’s more?! ◉ Event listeners ◉ Two-way binding ◉ Custom

    handling of (custom) attributes in (custom) views ◉ Keeping an eye out for changes ◉ Helping out RecyclerView
  4. Sample layout <layout xmlns:android="http://schemas.android.com/apk/res/android" > <data> <variable name="handlers" type="io.plaidapp.ui.DesignerNewsLogin" />

    </data> <AutoCompleteTextView android:text="@={credentials.username}" android:onEditorAction="@{(v, actionId, event) -> handlers.onNameEditorAction(actionId)}" android:onFocusChange="@{() -> handlers.onNameFocusChange()}" /> </layout>
  5. “ “Even if you don’t put IDs on those views

    we find them” - Yigit Boyar, explaining tags
  6. Bare minimum++ about_plaid.xml <ScrollView> <!-- more widgets --> <io.plaidapp.ui.widget.BaselineGridTextView android:id="@+id/about_description"

    /> </ScrollView> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <ScrollView> <!-- more widgets --> <io.plaidapp.ui.widget.BaselineGridTextView android:id="@+id/about_description" /> </ScrollView> </layout>
  7. Bare minimum++ AboutActivity.java // Inflate the layout AboutPlaidBinding binding =

    AboutPlaidBinding.inflate(layoutInflater); // Get the TextView plaidDescription = binding.aboutDescription; // Get the View to give back to PagerAdapter aboutPlaid = binding.getRoot(); @BindView(R.id.about_description) TextView plaidDescription; // Inflate the layout aboutPlaid = layoutInflater.inflate(R.layout.about_plaid, parent, false); ButterKnife.bind(this, aboutPlaid); // Inflate the layout AboutPlaidBinding binding = AboutPlaidBinding.inflate(layoutInflater); // Get the View to give back to PagerAdapter aboutPlaid = binding.getRoot(); // Get the TextView plaidDescription = binding.aboutDescription;
  8. Bare minimum++ Naming convention about_plaid.xml -> AboutPlaidBinding @+id/about_description -> aboutDescription

    Inflate layout via generated binding Compare with upstream: https://goo.gl/K2engR
  9. Breaking it down ◉ Page header ◉ List items ◉

    View types ◉ Image loading ◉ Link to Chrome Tab
  10. Setting up for success Convert the layouts for each view

    type Update your onCreateViewHolders Update your onBindViewHolders
  11. Our model public static class Library { public final String

    name; public final String link; public final String description; public final String imageUrl; public final boolean circleCrop; Library(String name, String description, String link, String imageUrl, boolean circleCrop) { this.name = name; this.description = description; this.link = link; this.imageUrl = imageUrl; this.circleCrop = circleCrop; } }
  12. Right into it Make XML understand model <data> <variable name="library"

    type="io.plaidapp.ui.AboutActivity.Library" /> </data> This allows us to use the model directly
  13. Right into it Make XML understand model <data> <variable name="library"

    type="io.plaidapp.ui.AboutActivity.Library" /> </data> <io.plaidapp.ui.widget.ForegroundRelativeLayout> <io.plaidapp.ui.widget.BaselineGridTextView android:text="@{library.name}"/> </io.plaidapp.ui.widget.ForegroundRelativeLayout>
  14. Introducing the moustache! Note the syntax! <data> <variable name="library" type="io.plaidapp.ui.AboutActivity.Library"

    /> </data> <io.plaidapp.ui.widget.ForegroundRelativeLayout> <io.plaidapp.ui.widget.BaselineGridTextView android:text="@{library.name}"/> </io.plaidapp.ui.widget.ForegroundRelativeLayout>
  15. Back into it Make XML understand clicks <data> <variable name="handlers"

    type="io.plaidapp.ui.AboutActivity.LibraryHolder" /> </data> And you can use that directly
  16. Back into it Make XML understand clicks <data> <variable name="handlers"

    type="io.plaidapp.ui.AboutActivity.LibraryHolder" /> </data> <io.plaidapp.ui.widget.ForegroundRelativeLayout> <Button android:id="@+id/library_link" android:onClick="@{() -> handlers.onLibraryLinkClick(library.link)}"/> </io.plaidapp.ui.widget.ForegroundRelativeLayout>
  17. And the avatar? Drop in a custom attribute <ImageView app:imageUrl="@{library.imageUrl}"

    /> Use Glide to load image @BindingAdapter("app:imageUrl") public static void setImageUrl(ImageView imageView, String url) { Glide.with()... }
  18. And the avatar? <ImageView app:imageUrl="@{library.imageUrl}" app:circleCrop="@{library.circleCrop}" /> Add some circle

    cropping Update the @BindingAdapter @BindingAdapter({"app:imageUrl", "app:circleCrop"}) public static void setAvatar(ImageView imageView, String url, boolean isCircleCropped) { Glide.with()... }
  19. And the avatar? Set optional @BindingAdapter parameters @BindingAdapter(value = {"app:imageUrl",

    "app:circleCrop"}, requireAll = false) public static void setAvatar(ImageView imageView, String url, boolean isCircleCropped) { } Easy to extend default widgets
  20. “ If you find yourself adding more and more logic

    to the XML, it’s time to move it to a @BindingAdapter.
  21. Putting it all together @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int

    viewType) { final LibraryBinding libraryBinding = LibraryBinding.inflate(getInflater(), parent, false); return new LibraryHolder(libraryBinding, host, circleCrop); } @Override public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) { ((LibraryHolder) holder).bind(libs[position - 1]); }
  22. Putting it all together public void bind(final Library lib) {

    libraryBinding.setLibrary(lib); } Set the model into the binding libraryBinding.executePendingBindings(); See it in more detail: https://goo.gl/Mxekr3
  23. I am watching you Validate user input private TextWatcher loginFieldWatcher

    = new TextWatcher() { @Override public void beforeTextChanged( … ) { } @Override public void onTextChanged( … ) { } @Override public void afterTextChanged(Editable s) { login.setEnabled(isLoginValid()); } }; username.addTextChangedListener(loginFieldWatcher);
  24. TextViewBindingAdapter to the rescue! @BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged", "android:afterTextChanged", "android:textAttrChanged"},

    requireAll = false) public static void setTextWatcher(TextView view, final BeforeTextChanged before, final OnTextChanged on, final AfterTextChanged after, final InverseBindingListener textAttrChanged) { ... } @BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged", "android:afterTextChanged", "android:textAttrChanged"}, requireAll = false) public static void setTextWatcher(TextView view, final BeforeTextChanged before, final OnTextChanged on, final AfterTextChanged after, final InverseBindingListener textAttrChanged) { ... }
  25. public void afterTextChanged() { login.setEnabled(isLoginValid()); } TextViewBindingAdapter to the rescue!

    Simplify in XML: <AutoCompleteTextView android:afterTextChanged="@{() -> handlers.afterTextChanged()}" … /> And in code:
  26. Charlie Chaplin moustache Create a model public static class DesignerNewsCredentials

    { public String username; public String password; } <AutoCompleteTextView android:id="@+id/username" android:text="@={credentials.username}" /> Assign input back <AutoCompleteTextView android:id="@+id/username" android:text="@={credentials.username}" />
  27. Observe it Make our model observable public static class DesignerNewsCredentials

    extends BaseObservable { private boolean hasCredentials; // Fields, constructors, getters and setters public void setPassword(String password) { this.password = password; setAllowLogin(hasUsernameAndPassword()); } public void setAllowLogin(boolean allowLogin) { this.allowLogin = allowLogin; notifyPropertyChanged(BR.hasCredentials); } } public static class DesignerNewsCredentials extends BaseObservable { private boolean hasCredentials; // Fields, constructors, getters and setters public void setPassword(String password) { this.password = password; setAllowLogin(hasUsernameAndPassword()); } public void setAllowLogin(boolean allowLogin) { this.allowLogin = allowLogin; notifyPropertyChanged(BR.hasCredentials); } }
  28. Still verbose though We can make it more succinct! public

    static class DesignerNewsCredentials extends BaseObservable { private boolean hasCredentials; // Fields, constructors, getters and setters public void setPassword(String password) { this.password = password; setAllowLogin(hasUsernameAndPassword()); } public void setAllowLogin(boolean hasCredentials) { this.hasCredentials = hasCredentials; notifyPropertyChanged(BR.hasCredentials); } } public static class DesignerNewsCredentials { public final ObservableBoolean hasCredentials = new ObservableBoolean(); // Fields, constructors, getters and setters public void setPassword(String password) { this.password = password; hasCredentials.set(hasUsernameAndPassword()); }
  29. Even more tricks Context is automatically imported for you android:background="@{ContextCompat.getColor(context,

    item.colour)}" Set default values android:text="@={credentials.username, default=Zarah}" String formatting and plurals android:text="@{@plurals/views(numViews, numViews)}"
  30. Even more tricks Setters in code but no attribute <android.support.v4.widget.DrawerLayout

    app:scrimColor="@{@color/scrim}" /> Maths in layout android:text="@{String.valueOf((float)24 + (float)2 * @dimen/spacing_normal)}"
  31. Caveats ◉ If in a library module, main project needs

    to enable it too ◉ That <include> thing ◉ That other <include> thing ◉ <merge> with qualified layouts
  32. Caveats Not too sweet escape (and a bonus false error)

    <variable name="list" type="java.util.List&lt;String&gt;" /> Misleading stacktrace /Users/zarah.dominguez/Android/plaid-databinding/app/src/main/java/io/plaidapp/ ui/AboutActivity.java Error:(52, 31) error: package io.plaidapp.databinding does not exist
  33. Any questions ? You can find me at ◉ @zarahjutz

    ◉ www.zdominguez.com Thanks!