Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Android DataBinding (ViewModel, UI Modularization and Testing)

945 views

Published on

2018. 11. 10 @GDG DevFest Seoul
2018. 11. 24 @GDG DevFest Busan

1. The reasons to use Data Binding
2. How Data Binding works (DirtyFlag)
3. UI Modularization
4. Unit Testing

Published in: Software
  • Be the first to comment

Android DataBinding (ViewModel, UI Modularization and Testing)

  1. 1. Yongjun Kim, kakaobank @imkimkevin Seoul Android DataBinding UI Modularization, ViewModel and Testing
  2. 2. AGENDA Korea ● The reasons to use Data Binding ● How Data Binding works ● UI Modularization ● Unit Testing
  3. 3. Warming UP! https://www.pexels.com
  4. 4. as Android Developer Activity (Fragment, View) Drawing UI Receiving User Interactions Service Broadcast Receiver Content Provider UI
  5. 5. as Android Developer Activity (Fragment, View) Drawing UI Receiving User Interactions Service Broadcast Receiver Content Provider ViewModel Repository Presenter And more…as API Developer Use Case Business UI Provides APIs
  6. 6. as Android Developer Activity (Fragment, View) Drawing UI Receiving User Interactions Service Broadcast Receiver Content Provider ViewModel Repository Presenter And more…as API Developer Use Case Business UI Provides APIs Testable Code
  7. 7. as Android Developer Activity (Fragment, View) Drawing UI Receiving User Interactions Service Broadcast Receiver Content Provider ViewModel Repository Presenter And more…as API Developer Use Case Business UI Provides APIs Testable Code Presenter
  8. 8. The reasons to use Data Binding Seoul
  9. 9. The past : Model-View-Presenter fun setTitleText(title: String) { titleText.text = title } fun setTitleVisible(visible: Boolean) { titleText.visibility = if (visible) View.VISIBLE else View.GONE } fun setButtonEnabled(enable: Boolean) { titleText.isEnabled = enable } Activity Presenter fun init() { if (isFirstLogin) { view.setTitleText("Welcome To DevFest 2018") view.setTitleVisible(true) } } fun validateDateOfBirth(number: String) { view.setButtonEnabled(number.length >= 6) } . . .
  10. 10. The past : Model-View-Presenter Activity Presenter fun init() { if (isFirstLogin) { view.setTitleText("Welcome To DevFest 2018") view.setTitleVisible(true) } } fun validateDateOfBirth(number: String) { view.setButtonEnabled(number.length >= 6) } Giant Glue Code if (isFirstLogin) { view.setTitleText("Welcome To DevFest 2018") view.setTitleVisible(true) } view.setButtonEnabled(number.length >= 6) fun setTitleText(title: String) { titleText.text = title } fun setTitleVisible(visible: Boolean) { titleText.visibility = if (visible) View.VISIBLE else View.GONE } fun setButtonEnabled(enable: Boolean) { titleText.isEnabled = enable } . . .
  11. 11. So far, It s ok BusinessUI Activity Activity PresenterActivity Presenter Presenter
  12. 12. So far, It s ok one to one but testable BusinessUI Activity Activity PresenterActivity Presenter Presenter
  13. 13. So far, It s ok one to one but testable non-reusable can be solved by - use case - abstraction BusinessUI Activity Activity PresenterActivity Presenter Presenter
  14. 14. So far, It s ok one to one but testable non-reusable can be solved by - use case - abstraction hard to maintain BusinessUI Activity Activity PresenterActivity Presenter Presenter - abstraction
  15. 15. DataBinding ● ? "16 https://unsplash.com
  16. 16. android { . . . dataBinding { enabled = true } } build.gradle in app module $gradle_version = Android Plugin for Gradle 1.5.0-alpha1 and higher android_studio_version = Android Studio 1.3 and higher // For Kotlin dependencies { kapt “com.android.databinding:compiler:$gradle_version" }
  17. 17. Sample Application
  18. 18. Sample Application
  19. 19. TextView Welcome message for Devfest Seoul of the year Requirements EditText 6 numbers for authentication Button Enabled for login with 6 numbers
  20. 20. override fun onCreate(savedInstanceState: Bundle?) { ... setContentView(R.layout.activity_main) textView.text = String.format(getString(R.string.title), presenter.year) editText.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(...) { } override fun afterTextChanged(s: Editable?) { presenter.validate(s.toString()) } override fun onTextChanged(...) { } }) button.setOnClickListener { } } fun setButtonEnabled(enable: Boolean) { button.isEnabled = enable } Activity
  21. 21. String.format(getString(R.string.title), presenter.year) override fun onCreate(savedInstanceState: Bundle?) { ... setContentView(R.layout.activity_main) textView.text = String.format(getString(R.string.title), presenter.year) editText.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(...) { } override fun afterTextChanged(s: Editable?) { presenter.validate(s.toString()) } override fun onTextChanged(...) { } }) button.setOnClickListener { } } fun setButtonEnabled(enable: Boolean) { button.isEnabled = enable } “Welcome to Devfest Seoul %s” Activity
  22. 22. override fun onCreate(savedInstanceState: Bundle?) { ... setContentView(R.layout.activity_main) textView.text = String.format(getString(R.string.title), presenter.year) editText.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(...) { } override fun afterTextChanged(s: Editable?) { presenter.validate(s.toString()) } override fun onTextChanged(...) { } }) button.setOnClickListener { } } fun setButtonEnabled(enable: Boolean) { button.isEnabled = enable } presenter.year presenter.validate(s.toString()) fun validate(number: String) { view.setButtonEnabled(number.isCodeValid()) } Activity Presenter
  23. 23. override fun onCreate(savedInstanceState: Bundle?) { ... setContentView(R.layout.activity_main) textView.text = String.format(getString(R.string.title), presenter.year) editText.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(...) { } override fun afterTextChanged(s: Editable?) { presenter.validate(s.toString()) } override fun onTextChanged(...) { } }) button.setOnClickListener { } } fun setButtonEnabled(enable: Boolean) { button.isEnabled = enable } presenter.year presenter.validate(s.toString()) fun validate(number: String) { view.setButtonEnabled(number.isCodeValid()) } fun setButtonEnabled(enable: Boolean) { button.isEnabled = enable } Activity Presenter
  24. 24. Activity view xml Presenter Data Binding MVP with Data Binding
  25. 25. override fun onCreate(savedInstanceState: Bundle?) { ... setContentView(R.layout.activity_main) textView.text = String.format(getString(R.string.title), presenter.year) editText.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(...) { } override fun afterTextChanged(s: Editable?) { presenter.validate(s.toString()) } override fun onTextChanged(...) { } }) button.setOnClickListener { } } fun setButtonEnabled(enable: Boolean) { button.isEnabled = enable } Activity Presenterxml Data Binding
  26. 26. override fun onCreate(savedInstanceState: Bundle?) { ... setContentView(R.layout.activity_main) textView.text = String.format(getString(R.string.title), presenter.year) editText.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(...) { } override fun afterTextChanged(s: Editable?) { presenter.validate(s.toString()) } override fun onTextChanged(...) { } }) button.setOnClickListener { } } fun setButtonEnabled(enable: Boolean) { button.isEnabled = enable } setContentView(R.layout.activity_main) val binding = DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main) binding.setLifecycleOwner(this) // LiveData binding.setPresenter(presenter) Activity Presenterxml Data Binding val binding = DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main) binding.setLifecycleOwner(this) // LiveData
  27. 27. override fun onCreate(savedInstanceState: Bundle?) { ... setContentView(R.layout.activity_main) textView.text = String.format(getString(R.string.title), presenter.year) editText.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(...) { } override fun afterTextChanged(s: Editable?) { presenter.validate(s.toString()) } override fun onTextChanged(...) { } }) button.setOnClickListener { } } fun setButtonEnabled(enable: Boolean) { button.isEnabled = enable } setContentView(R.layout.activity_main) val binding = DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main) binding.setLifecycleOwner(this) // LiveData binding.setPresenter(presenter) <layout> <android.support.constraint.ConstraintLayout …> <TextView android:id=“@+id/textView" ... /> <EditText android:id=“@+id/editText" ... /> <Button android:id=“@+id/button" ... /> </android.support.constraint.ConstraintLayout> </layout> <layout> </layout> R.layout.activity_main Activity Presenterxml Data Binding
  28. 28. override fun onCreate(savedInstanceState: Bundle?) { ... setContentView(R.layout.activity_main) textView.text = String.format(getString(R.string.title), presenter.year) editText.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(...) { } override fun afterTextChanged(s: Editable?) { presenter.validate(s.toString()) } override fun onTextChanged(...) { } }) button.setOnClickListener { } } fun setButtonEnabled(enable: Boolean) { button.isEnabled = enable } setContentView(R.layout.activity_main) val binding = DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main) binding.setLifecycleOwner(this) // LiveData binding.setPresenter(presenter)binding.setPresenter(presenter) <layout> <data> <variable name=“presenter” type=“com...MainPresenter"/> </data> <android.support.constraint.ConstraintLayout …> <EditText android:id=“@+id/editText" ... /> <data> <variable name=“presenter” type=“com...MainPresenter"/> </data> Activity Presenterxml Data Binding
  29. 29. delete override fun onCreate(savedInstanceState: Bundle?) { ... val binding = DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main) binding.setLifecycleOwner(this) binding.setPresenter(presenter) textView.text = String.format(getString(R.string.title), presenter.year) editText.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(...) { } override fun afterTextChanged(s: Editable?) { presenter.validate(s.toString()) } override fun onTextChanged(...) { } }) button.setOnClickListener { } } fun setButtonEnabled(enable: Boolean) { button.isEnabled = enable } Activity Presenterxml Data Binding
  30. 30. https://www.pexels.com
  31. 31. Activity Presenterxml Data Binding override fun onCreate(savedInstanceState: Bundle?) { ... val binding = DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main) binding.setLifecycleOwner(this) binding.setPresenter(presenter) textView.text = String.format(getString(R.string.title), presenter.year) editText.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(...) { } override fun afterTextChanged(s: Editable?) { presenter.validate(s.toString()) } override fun onTextChanged(...) { } }) button.setOnClickListener { } } fun setButtonEnabled(enable: Boolean) { button.isEnabled = enable } <TextView <layout> ... <android.support.constraint.ConstraintLayout ...> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text=“@{@string/title(presenter.year)}” />android:text=“@{@string/title(presenter.year)}” textView.text = String.format(getString(R.string.title), presenter.year) TextView
  32. 32. android:text=“@{@string/title(presenter.year)}” Expression Language
  33. 33. android:text="@{String.valueOf(index + 1)}" android:visibility="@{size < 15 ? View.GONE : View.VISIBLE}” android:textColor=“@{colorText}” ??? android:text=“@{@string/title(presenter.year)}” Expression Language
  34. 34. android:textColor=“#fe3dfc” @InverseMethod
  35. 35. colorText.value = "#000000" android:textColor=“#fe3dfc” android:textColor=“@{colorText}” @InverseMethod
  36. 36. android:textColor=“#fe3dfc” android:textColor=“@{colorText}” // ERROR!! @InverseMethod colorText.value = "#000000"
  37. 37. object Converters { @JvmStatic @InverseMethod(“toColor") fun fromColorString(colorString: String): Int { return Color.parseColor(colorString) } @JvmStatic // Two-way Binding fun toColor(color: Int): String { return color.toString() } } android:textColor="@{Converters.fromColorString(colorText)}" @InverseMethod
  38. 38. android:text="@{String.valueOf(index + 1)}" android:visibility="@{size < 15 ? View.GONE : View.VISIBLE}” android:textColor=“@{colorText}” // @InverseMethod android:visibility=“@{title != null}” ??? android:text=“@{@string/title(presenter.year)}” Expression Language
  39. 39. android:visibility=“@{title != null}” visible | invisible | gone @BindingConversion object Conversions { @JvmStatic @BindingConversion fun convertBooleanToVisibility(visible: Boolean) = if (visible) View.VISIBLE else View.GONE }
  40. 40. android:visibility=“@{title != null}” visible | invisible | gone Normally 1. visible | invisible ??? 2. visible | gone ???
  41. 41. android:visibility=“@{title != null}” visible | invisible | gone Normally 1. visible | invisible 2. visible | gone app:visible=“@{title != null}” app:visibleOrGone=“@{title != null}”
  42. 42. @BindingAdapter @Target(ElementType.METHOD) public @interface BindingAdapter { String[] value(); boolean requireAll() default true; }
  43. 43. @BindingAdapter @Target(ElementType.METHOD) public @interface BindingAdapter { String[] value(); boolean requireAll() default true; } // Example @BindingAdapter("visible") @BindingAdapter(value={"url", "size"}, requireAll=false)
  44. 44. app:visible=“@{title != null}” app:visibleOrGone=“@{title != null}” @BindingAdapter @JvmStatic @BindingAdapter("visible") fun View.setVisible(visible: Boolean) { visibility = if (visible) View.VISIBLE else View.INVISIBLE } @JvmStatic @BindingAdapter("visibleOrGone") fun View.setVisibleOrGone(visible: Boolean) { visibility = if (visible) View.VISIBLE else View.GONE }
  45. 45. Activity Presenterxml Data Binding override fun onCreate(savedInstanceState: Bundle?) { ... val binding = DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main) binding.setLifecycleOwner(this) binding.setPresenter(presenter) editText.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(...) { } override fun afterTextChanged(s: Editable?) { presenter.validate(s.toString()) } override fun onTextChanged(...) { } }) button.setOnClickListener { } } fun setButtonEnabled(enable: Boolean) { button.isEnabled = enable } editText.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(...) { } override fun afterTextChanged(s: Editable?) { presenter.validate(s.toString()) } override fun onTextChanged(...) { } }) <layout> ... <android.support.constraint.ConstraintLayout ...> <EditText android:id=“@+id/editText" android:text=“@={presenter.authCode}”android:text=“@={presenter.authCode}” EditText class MainPresenter { val authCode = MutableLiveData<String>() ... }
  46. 46. android:text=“@={presenter.authCode)}” Two-way data binding
  47. 47. android:text=“@={presenter.authCode)}” Two-way data binding OK presenter.authCode = null
  48. 48. android:text=“@={presenter.authCode)}” Two-way data binding OKOK 123456 presenter.authCode = “123456”
  49. 49. android:text=“@={presenter.authCode)}” Two-way data binding OK 1234 OK presenter.authCode = “1234”
  50. 50. android:text=“@={presenter.authCode)}” InverseBindingAdapter in ActivityMainBinding.java @InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged") public static String getTextString(TextView view) { return view.getText().toString(); }
  51. 51. android:text=“@={presenter.authCode)}” TextViewBindingAdapter.setTextWatcher(this.editText, null, null, null, editTextandroidTextAttrChanged); BindingAdapter in ActivityMainBinding.java private InverseBindingListener editTextandroidTextAttrChanged = new InverseBindingListener() { @Override public void onChange() { String callbackArg_0 = TextViewBindingAdapter.getTextString(editText); ... vmAuthCode.setValue(((String) (callbackArg_0))); } editTextandroidTextAttrChanged
  52. 52. android:text=“@={presenter.authCode)}” TextViewBindingAdapter.setTextWatcher(this.editText, null, null, null, editTextandroidTextAttrChanged); BindingAdapter in ActivityMainBinding.java private InverseBindingListener editTextandroidTextAttrChanged = new InverseBindingListener() { @Override public void onChange() { String callbackArg_0 = TextViewBindingAdapter.getTextString(editText); ... vmAuthCode.setValue(((String) (callbackArg_0))); } editTextandroidTextAttrChanged TextViewBindingAdapter.getTextString(editText);
  53. 53. android:text=“@={presenter.authCode)}” TextViewBindingAdapter.setTextWatcher(this.editText, null, null, null, editTextandroidTextAttrChanged); BindingAdapter in ActivityMainBinding.java TextViewBindingAdapter.setTextWatcher @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) { final TextWatcher newValue; ... newValue = new TextWatcher() { ... @Override public void onTextChanged(CharSequence s, int start, int before, int count) if (textAttrChanged != null) { textAttrChanged.onChange();
  54. 54. android:text=“@={presenter.authCode)}” Default BindingAdapters android.databinding.adapters @BindingMethods({ @BindingMethod(type = TextView.class, attribute = "android:inputType", method = "setRawInputType"), @BindingMethod(type = TextView.class, attribute = "android:textAllCaps", method = “setAllCaps"), ... }) public class TextViewBindingAdapter { @BindingAdapter("android:text") public static void setText(TextView view, CharSequence text) { final CharSequence oldText = view.getText(); if (text == oldText || (text == null && oldText.length() == 0)) { return; }
  55. 55. Activity Presenterxml Data Binding override fun onCreate(savedInstanceState: Bundle?) { ... val binding = DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main) binding.setLifecycleOwner(this) binding.setPresenter(presenter) button.setOnClickListener { } } fun setButtonEnabled(enable: Boolean) { button.isEnabled = enable } button.setOnClickListener { } fun setButtonEnabled(enable: Boolean) { button.isEnabled = enable } <Button … android:enabled="@{ValidationUtil.isCodeValid(vm.authCode)}" android:onClick="@{() -> vm.login()}" <layout> <data> <import type=“com....ValidationUtil"/> <variable <import type=“com....ValidationUtil"/> object ValidationUtil { @JvmStatic fun String?.isCodeValid() = this?.length ?: 0 >= 6 }
  56. 56. class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main) binding.setLifecycleOwner(this) binding.setPresenter(presenter) } } Activity
  57. 57. Activity xml <layout> <data> <import type=“com....ValidationUtil"/> <variable name=“presenter” type=“com...MainPresenter"/> </data> <android.support.constraint.ConstraintLayout ...> <TextView android:text=“@{@string/title(presenter.year)}” .../> <EditText android:text=“@={presenter.authCode}” .../> <Button android:enabled=“@{ValidationUtil.isCodeValid(presenter.authCode)}” android:onClick="@{() -> presenter.login()}” .../> </android.support.constraint.ConstraintLayout> </layout>
  58. 58. class MainPresenter { var year : String val authCode = MutableLiveData<String>() init { year = "2018" } fun login() { // do something! } } Activity Presenterxml Data Binding
  59. 59. Activity view xml Presenter Data Binding AS-IS : MVP with Data Binding
  60. 60. class MainViewModel { var year : String val authCode = MutableLiveData<String>() init { year = "2018" } fun login() { // do something! } } Activity ViewModelxml Data Binding
  61. 61. TO-BE : MVVM with Data Binding Activity view xml ViewModel Data Binding
  62. 62. How DataBinding works Seoul
  63. 63. OK <android.support.constraint.ConstraintLayout ...> ... <EditText android:id=“@+id/editText“ android:text=“@={vm.authCode}” .../> <Button android:id=“@+id/button“ android:enabled="@{ValidationUtil.isCodeValid(vm.authCode)}"
  64. 64. OK OK 123456 <android.support.constraint.ConstraintLayout ...> ... <EditText android:id=“@+id/editText“ android:text=“@={vm.authCode}” .../> <Button android:id=“@+id/button“ android:enabled="@{ValidationUtil.isCodeValid(vm.authCode)}"
  65. 65. OK OK 123456 <android.support.constraint.ConstraintLayout ...> ... <EditText android:id=“@+id/editText“ android:text=“@={vm.authCode}” .../> <Button android:id=“@+id/button“ android:enabled="@{ValidationUtil.isCodeValid(vm.authCode)}" How this works in generated class?
  66. 66. OK OK 123456 <android.support.constraint.ConstraintLayout ...> ... <EditText android:id=“@+id/editText“ android:text=“@={vm.authCode}” .../> <Button android:id=“@+id/button“ android:enabled="@{ValidationUtil.isCodeValid(vm.authCode)}" DirtyFlag
  67. 67. activity_main.xml <layout> <data> <import type=“com....ValidationUtil"/> <variable name=“vm” type=“com...MainViewModel"/> </data> <android.support.constraint.ConstraintLayout ...> <TextView android:text=“@{@string/title(vm.year)}” .../> <EditText android:text=“@={presenter.authCode}” .../> <Button android:enabled=“@{ValidationUtil.isCodeValid(vm.authCode)}” android:onClick="@{() -> vm.login()}” .../> </android.support.constraint.ConstraintLayout> </layout>
  68. 68. activity_main.xml Layout Processor <layout> <data> <import type=“com....ValidationUtil"/> <variable name=“vm” type=“com...MainViewModel"/> </data> <android.support.constraint.ConstraintLayout ...> <TextView android:text=“@{@string/title(vm.year)}” .../> <EditText android:text=“@={presenter.authCode}” .../> <Button android:enabled=“@{ValidationUtil.isCodeValid(vm.authCode)}” android:onClick="@{() -> vm.login()}” .../> </android.support.constraint.ConstraintLayout> </layout>
  69. 69. ActivityMainBinding.java Layout Processor public class ActivityMainTestBinding extends android.databinding.ViewDataBinding implements android.databinding.generated.callback.OnClickListener.Listener { ... @NonNull public final android.widget.Button button; @NonNull public final android.widget.EditText editText; @NonNull public final android.widget.TextView textView; @NonNull private final android.support.constraint.ConstraintLayout mboundView0; @Nullable private com.github.kimkevin.devfestseoul18.MainViewModel mVm; @Nullable private final android.view.View.OnClickListener mCallback1; Variable Listener View @NonNull public final android.widget.Button button; @NonNull public final android.widget.EditText editText; @NonNull public final android.widget.TextView textView; @Nullable private com.github.kimkevin.devfestseoul18.MainViewModel mVm; @Nullable private final android.view.View.OnClickListener mCallback1; ActivityMainTestBinding android.databinding.ViewDataBinding
  70. 70. DirtyFlag mapping Dealing with updates When data is changed, mDirtyFlags is updated 0 0 1 0 1 0 1 0 0 0x1L 0x2L 0x4L /* flag mapping flag 0 : vm.authCode flag 1 : vm flag 2 : null flag mapping end*/ ActivityMainBinding.java
  71. 71. DirtyFlag mapping Dealing with updates When data is changed, mDirtyFlags is updated 0 0 1 0 1 0 1 0 0 0x1L 0x2L 0x4L /* flag mapping flag 0 : vm.authCode flag 1 : vm flag 2 : initialized flag mapping end*/ ActivityMainBinding.java
  72. 72. /* flag mapping vm.authCode : 0x1L vm : 0x2L initialized : 0x4L flag mapping end*/ private boolean onChangeVmAuthCode(MutableLiveData<String> VmAuthCode, int fieldId) { if (fieldId == BR._all) { synchronized(this) { mDirtyFlags |= 0x1L; } return true; } return false; ActivityMainBinding.java mDirtyFlag vm.authCode : 0x1L
  73. 73. /* flag mapping vm.authCode : 0x1L vm : 0x2L initialized : 0x4L flag mapping end*/ ActivityMainBinding.java mDirtyFlag vm : 0x2L public void setVm(@Nullable com...MainViewModel Vm) { this.mVm = Vm; synchronized(this) { mDirtyFlags |= 0x2L; } notifyPropertyChanged(BR.vm); super.requestRebind(); }
  74. 74. /* flag mapping vm.authCode : 0x1L vm : 0x2L initialized : 0x4L flag mapping end*/ ActivityMainBinding.java mDirtyFlag @Override public void invalidateAll() { synchronized(this) { mDirtyFlags = 0x4L; } requestRebind(); } initialized : 0x4L
  75. 75. ActivityMainBinding.java flag initialized : 0x4L class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val vm = MainViewModel() val binding = DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main) binding.setLifecycleOwner(this) binding.setVm(viewModel) } } mDirtyFlags = 0x4L
  76. 76. ActivityMainBinding.java flag vm : 0x2L class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val vm = MainViewModel() val binding = DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main) binding.setLifecycleOwner(this) binding.setVm(viewModel) } } mDirtyFlags = 0x4L | 0x2L = 0x6L
  77. 77. @Override protected void executeBindings() { ... dirtyFlags = mDirtyFlags; if ((dirtyFlags & 0x7L) != 0) { if ((dirtyFlags & 0x6L) != 0) { textViewAndroidStringTitleVmYear = textView...getString(R.string.title, vmYear); } // read vm.authCode.getValue() vmAuthCodeGetValue = vmAuthCode.getValue(); // read ValidationUtil.isCodeValid(vm.authCode.getValue()) vmAuthCodeLengthInt6 = ValidationUtil.isCodeValid(vmAuthCodeLength); } if ((dirtyFlags & 0x7L) != 0) { this.button.setEnabled(vmAuthCodeLengthInt6); TextViewBindingAdapter.setText(this.editText, vmAuthCodeGetValue); } if ((dirtyFlags & 0x4L) != 0) { this.button.setOnClickListener(mCallback1); TextViewBindingAdapter.setTextWatcher(this.editText, null, null, null, editTextandroidTextAttrChanged); } if ((dirtyFlags & 0x6L) != 0) { TextViewBindingAdapter.setText(this.textView, textViewAndroidStringTitleVmYear); } } mDirtyFlags = 0x6L
  78. 78. @Override protected void executeBindings() { ... dirtyFlags = mDirtyFlags; if ((0x6L & 0x7L) != 0) { if ((0x6L & 0x6L) != 0) { textViewAndroidStringTitleVmYear = textView...getString(R.string.title, vmYear); } // read vm.authCode.getValue() vmAuthCodeGetValue = vmAuthCode.getValue(); // read ValidationUtil.isCodeValid(vm.authCode.getValue()) vmAuthCodeLengthInt6 = ValidationUtil.isCodeValid(vmAuthCodeLength); } if ((0x6L & 0x7L) != 0) { this.button.setEnabled(vmAuthCodeLengthInt6); TextViewBindingAdapter.setText(this.editText, vmAuthCodeGetValue); } if ((0x6L & 0x4L) != 0) { this.button.setOnClickListener(mCallback1); TextViewBindingAdapter.setTextWatcher(this.editText, null, null, null, editTextandroidTextAttrChanged); } if ((0x6L & 0x6L) != 0) { TextViewBindingAdapter.setText(this.textView, textViewAndroidStringTitleVmYear); } } mDirtyFlags = 0x6L
  79. 79. @Override protected void executeBindings() { ... dirtyFlags = mDirtyFlags; if ((0x6L & 0x7L) != 0) { if ((0x4L & 0x6L) != 0) { textViewAndroidStringTitleVmYear = textView...getString(R.string.title, vmYear); } // read vm.authCode.getValue() vmAuthCodeGetValue = vmAuthCode.getValue(); // read ValidationUtil.isCodeValid(vm.authCode.getValue()) vmAuthCodeLengthInt6 = ValidationUtil.isCodeValid(vmAuthCodeLength); } if ((0x4L & 0x7L) != 0) { this.button.setEnabled(vmAuthCodeLengthInt6); TextViewBindingAdapter.setText(this.editText, vmAuthCodeGetValue); } if ((0x4L & 0x4L) != 0) { this.button.setOnClickListener(mCallback1); TextViewBindingAdapter.setTextWatcher(this.editText, null, null, null, editTextandroidTextAttrChanged); } if ((0x4L & 0x6L) != 0) { TextViewBindingAdapter.setText(this.textView, textViewAndroidStringTitleVmYear); } } mDirtyFlags = 0x4L 1 1 0 1 1 1 0x6L 0x7L (0x6L & 0x7L) != 0 &
  80. 80. @Override protected void executeBindings() { ... dirtyFlags = mDirtyFlags; if ((0x6L & 0x7L) != 0) { if ((0x4L & 0x6L) != 0) { textViewAndroidStringTitleVmYear = textView...getString(R.string.title, vmYear); } // read vm.authCode.getValue() vmAuthCodeGetValue = vmAuthCode.getValue(); // read ValidationUtil.isCodeValid(vm.authCode.getValue()) vmAuthCodeLengthInt6 = ValidationUtil.isCodeValid(vmAuthCodeLength); } if ((0x4L & 0x7L) != 0) { this.button.setEnabled(vmAuthCodeLengthInt6); TextViewBindingAdapter.setText(this.editText, vmAuthCodeGetValue); } if ((0x4L & 0x4L) != 0) { this.button.setOnClickListener(mCallback1); TextViewBindingAdapter.setTextWatcher(this.editText, null, null, null, editTextandroidTextAttrChanged); } if ((0x4L & 0x6L) != 0) { TextViewBindingAdapter.setText(this.textView, textViewAndroidStringTitleVmYear); } } (0x6L & 0x7L) != 0 mDirtyFlags = 0x4L 1 1 1 0 0x6L 0x7L & 1 1 0
  81. 81. @Override protected void executeBindings() { ... dirtyFlags = mDirtyFlags; if ((0x6L & 0x7L) != 0) { if ((0x4L & 0x6L) != 0) { textViewAndroidStringTitleVmYear = textView...getString(R.string.title, vmYear); } // read vm.authCode.getValue() vmAuthCodeGetValue = vmAuthCode.getValue(); // read ValidationUtil.isCodeValid(vm.authCode.getValue()) vmAuthCodeLengthInt6 = ValidationUtil.isCodeValid(vmAuthCodeLength); } if ((0x4L & 0x7L) != 0) { this.button.setEnabled(vmAuthCodeLengthInt6); TextViewBindingAdapter.setText(this.editText, vmAuthCodeGetValue); } if ((0x4L & 0x4L) != 0) { this.button.setOnClickListener(mCallback1); TextViewBindingAdapter.setTextWatcher(this.editText, null, null, null, editTextandroidTextAttrChanged); } if ((0x4L & 0x6L) != 0) { TextViewBindingAdapter.setText(this.textView, textViewAndroidStringTitleVmYear); } } (0x6L & 0x7L) != 0 mDirtyFlags = 0x4L 1 1 1 1 1 0 0x7L 0x6L & 0x6L 1 1 0
  82. 82. @Override protected void executeBindings() { ... dirtyFlags = mDirtyFlags; if ((0x6L & 0x7L) != 0) { if ((0x4L & 0x6L) != 0) { textViewAndroidStringTitleVmYear = textView...getString(R.string.title, vmYear); } // read vm.authCode.getValue() vmAuthCodeGetValue = vmAuthCode.getValue(); // read ValidationUtil.isCodeValid(vm.authCode.getValue()) vmAuthCodeLengthInt6 = ValidationUtil.isCodeValid(vmAuthCodeLength); } if ((0x4L & 0x7L) != 0) { this.button.setEnabled(vmAuthCodeLengthInt6); TextViewBindingAdapter.setText(this.editText, vmAuthCodeGetValue); } if ((0x4L & 0x4L) != 0) { this.button.setOnClickListener(mCallback1); TextViewBindingAdapter.setTextWatcher(this.editText, null, null, null, editTextandroidTextAttrChanged); } if ((0x4L & 0x6L) != 0) { TextViewBindingAdapter.setText(this.textView, textViewAndroidStringTitleVmYear); } } (0x6L & 0x7L) != 0 mDirtyFlags = 0x4L 1 1 1 1 1 0 0x7L 0x6L & 0x6L 1 1 0 != 0 true
  83. 83. @Override protected void executeBindings() { ... dirtyFlags = mDirtyFlags; if (true) { if (true) { textViewAndroidStringTitleVmYear = textView...getString(R.string.title, vmYear); } // read vm.authCode.getValue() vmAuthCodeGetValue = vmAuthCode.getValue(); // read ValidationUtil.isCodeValid(vm.authCode.getValue()) vmAuthCodeLengthInt6 = ValidationUtil.isCodeValid(vmAuthCodeLength); } if (true) { this.button.setEnabled(vmAuthCodeLengthInt6); TextViewBindingAdapter.setText(this.editText, vmAuthCodeGetValue); } if (true) { this.button.setOnClickListener(mCallback1); TextViewBindingAdapter.setTextWatcher(this.editText, null, null, null, editTextandroidTextAttrChanged); } if (true) { TextViewBindingAdapter.setText(this.textView, textViewAndroidStringTitleVmYear); } } mDirtyFlags = 0x6L
  84. 84. Binding - Initialized - set Variables OK
  85. 85. 1 OK ActivityMainBinding.java flag vm.authCode : 0x1L Input 1
  86. 86. 1 OK ActivityMainBinding.java flag vm.authCode : 0x1L mDirtyFlags = 0x1L
  87. 87. @Override protected void executeBindings() { ... dirtyFlags = mDirtyFlags; if ((0x1L & 0x7L) != 0) { if ((0x1L & 0x6L) != 0) { textViewAndroidStringTitleVmYear = textView...getString(R.string.title, vmYear); } // read vm.authCode.getValue() vmAuthCodeGetValue = vmAuthCode.getValue(); // read ValidationUtil.isCodeValid(vm.authCode.getValue()) vmAuthCodeLengthInt6 = ValidationUtil.isCodeValid(vmAuthCodeLength); } if ((0x1L & 0x7L) != 0) { this.button.setEnabled(vmAuthCodeLengthInt6); TextViewBindingAdapter.setText(this.editText, vmAuthCodeGetValue); } if ((0x1L & 0x4L) != 0) { this.button.setOnClickListener(mCallback1); TextViewBindingAdapter.setTextWatcher(this.editText, null, null, null, editTextandroidTextAttrChanged); } if ((0x1L & 0x6L) != 0) { TextViewBindingAdapter.setText(this.textView, textViewAndroidStringTitleVmYear); } } mDirtyFlags = 0x1L
  88. 88. @Override protected void executeBindings() { ... dirtyFlags = mDirtyFlags; if (true) { if (false) { textViewAndroidStringTitleVmYear = textView...getString(R.string.title, vmYear); } // read vm.authCode.getValue() vmAuthCodeGetValue = vmAuthCode.getValue(); // read ValidationUtil.isCodeValid(vm.authCode.getValue()) vmAuthCodeLengthInt6 = ValidationUtil.isCodeValid(vmAuthCodeLength); } if (true) { this.button.setEnabled(vmAuthCodeLengthInt6); TextViewBindingAdapter.setText(this.editText, vmAuthCodeGetValue); } if (false) { this.button.setOnClickListener(mCallback1); TextViewBindingAdapter.setTextWatcher(this.editText, null, null, null, editTextandroidTextAttrChanged); } if (false) { TextViewBindingAdapter.setText(this.textView, textViewAndroidStringTitleVmYear); } } mDirtyFlags = 0x1L @Override protected void executeBindings() { ... dirtyFlags = mDirtyFlags; if (true) { if (false) { } // read vm.authCode.getValue() vmAuthCodeGetValue = vmAuthCode.getValue(); // read ValidationUtil.isCodeValid(vm.authCode.getValue()) vmAuthCodeLengthInt6 = ValidationUtil.isCodeValid(vmAuthCodeLength); } if (true) { this.button.setEnabled(vmAuthCodeLengthInt6); TextViewBindingAdapter.setText(this.editText, vmAuthCodeGetValue); } if (false) { } if (false) { } }
  89. 89. UI Modularization Seoul
  90. 90. Reusable View
  91. 91. ItemScheduleinding.java
  92. 92. "95 Includes data class SpeakerSchedule(val name: String, val subject: String) <layout> <data> <variable name="schedule" type=“com...SpeakerSchedule” /> </data> <android.support.constraint.ConstraintLayout ...> <TextView android:id="@+id/subjectTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text=“@{schedule.subject}"/> <TextView android:id="@+id/nameTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text=“@{schedule.name}" /> ... </android.support.constraint.ConstraintLayout> </layout> item_schedule.xml
  93. 93. "96 Includes <layout> <data> <variable name="schedule" type="com...SpeakerSchedule" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> ... <include android:layout=“@layout/item_schedule” android:layout_width="wrap_content" android:layout_height="wrap_content" app:schedule=“@{schedule}” /> ... </android.support.constraint.ConstraintLayout> </layout> <include android:layout=“@layout/item_schedule” app:schedule=“@{schedule}” /> <variable name="schedule" type="com...SpeakerSchedule" />
  94. 94. Includes binding.schedule = SpeakerSchedule(“KimKevin”, “Android...”) <include android:layout=“@layout/item_schedule” app:schedule=“@{schedule}” .../> <variable name="schedule" type=“com.github.kimkevin…SpeakerSchedule" />
  95. 95. Listing & BindingAdapter @BindingAdapter("schedules") fun setSchedules(container: LinearLayout, schedules: List<SpeakerSchedule>) { container.removeAllViews() val inflater = container.context.getSystemService( Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater schedules.forEach { val binding = DataBindingUtil.inflate<ItemScheduleBinding>( inflater, R.layout.item_schedule, container, true) binding.schedule = it } }
  96. 96. Listing & BindingAdapter <LinearLayout android:orientation="vertical" app:schedules="@{schedules}" .../> <variable name="schedules" type="List&lt;SpeakerSchedule&gt;" /> val schedules = mutableListOf<SpeakerSchedule>().apply { add(SpeakerSchedule("Kevin", "Android")) add(SpeakerSchedule("David", “Data Binding”)) } binding.schedule = schedules
  97. 97. "100 Listing & ViewHolder
  98. 98. Listing & ViewHolder ItemViewHolder class ItemViewHolder(v: View) : RecyclerView.ViewHolder
  99. 99. Listing & ViewHolder HeaderViewHolder ItemViewHolder class ItemViewHolder(v: View) : RecyclerView.ViewHolder class HeaderViewHolder(v: View) : RecyclerView.ViewHolder
  100. 100. Listing & ViewHolder HeaderViewHolder ItemViewHolder ProgressViewHolder class ItemViewHolder(v: View) : RecyclerView.ViewHolder class HeaderViewHolder(v: View) : RecyclerView.ViewHolder class ProgressViewHolder(v: View) : RecyclerView.ViewHolder
  101. 101. class BindingViewHolder<T: ViewDataBinding> constructor(val binding: T): RecyclerView.ViewHolder(binding.root)
  102. 102. class BindingViewHolder<T: ViewDataBinding> constructor(val binding: T): RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, position: Int): BindingViewHolder<ViewDataBinding> { return BindingViewHolder(ItemScheduleBinding.inflate( LayoutInflater.from(parent.context), parent, false)) } override fun onBindViewHolder( viewHolder: BindingViewHolder<ViewDataBinding>, position: Int) { val binding = viewHolder.binding as ItemScheduleBinding binding.schedule = items.get(position) } Adapter
  103. 103. Unit Testing Seoul
  104. 104. TextView Welcome message for Devfest Seoul of the year Requirements EditText 6 numbers for authentication Button Enabled for login with 6 numbers
  105. 105. TextView Welcome message for Devfest Seoul of the year Requirements EditText 6 numbers for authentication Button Enabled for login with 6 numbers Business UI
  106. 106. <TextView android:text=“@{@string/title(vm.year)}” .../>
  107. 107. ??? <TextView android:text=“@{@string/title(vm.year)}” .../> Business UI
  108. 108. ??? <TextView android:text=“@{@string/title(vm.year)}” .../> Business UI vm.year
  109. 109. <TextView android:text=“@{@string/title(vm.year)}” .../> Business UI @Test fun initVm_setYear() { vm = MainViewModel() assertTrue(vm.year, deafest.year) } val devfest = repository.getDevfest() vm.year = devfest.year MainViewModel
  110. 110. <Button android:enabled=“@{vm.authCode >= 6}” .../>
  111. 111. Business UI <Button android:enabled=“@{vm.authCode >= 6}” .../>
  112. 112. ??? <Button android:enabled=“@{vm.authCode >= 6}” .../> Business UI
  113. 113. vm.authCode >= 6 <Button android:enabled=“@{vm.authCode >= 6}” .../> Business UI
  114. 114. fun String?.isCodeValid() = this?.length ?: 0 >= 6 <Button android:enabled=“@{ValidationUtil.isCodeValid(vm.authCode)}” .../> Business UI
  115. 115. <Button android:enabled=“@{ValidationUtil.isCodeValid(vm.authCode)}” .../> @Test fun validateLessThan6Numbers() { assertFalse(“1234".isCodeValid()) } Business UI
  116. 116. <Button android:enabled=“@{ValidationUtil.isCodeValid(vm.authCode)}” .../> @Test fun validate6Numbers() { assertTrue("123456".isCodeValid()) } Business UI
  117. 117. Seoul Thank you Yongjun Kim, kakaobank @imkimkevin
  118. 118. Q&A imkimkevin@gmail.com https://www.facebook.com/imkimkevin Busan

×