Android DataBinding (ViewModel, UI Modularization and Testing)

Yongjun Kim
Yongjun KimSoftware Engineer at Kakaobank
Yongjun Kim, kakaobank
@imkimkevin
Seoul
Android DataBinding
UI Modularization, ViewModel and Testing
AGENDA
Korea
● The reasons to use Data Binding
● How Data Binding works
● UI Modularization
● Unit Testing
Warming UP!
https://www.pexels.com
Android DataBinding (ViewModel, UI Modularization and Testing)
as Android Developer
Activity (Fragment, View)
Drawing UI
Receiving User Interactions Service Broadcast Receiver Content Provider
UI
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
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
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
The reasons to use
Data Binding
Seoul
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)
}
.
.
.
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
}
.
.
.
So far, It s ok
BusinessUI
Activity
Activity
PresenterActivity
Presenter
Presenter
So far, It s ok
one to one
but testable
BusinessUI
Activity
Activity
PresenterActivity
Presenter
Presenter
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
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
DataBinding
● ?
"16
https://unsplash.com
Android DataBinding (ViewModel, UI Modularization and Testing)
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"
}
Sample Application
Sample Application
TextView
Welcome message
for Devfest Seoul of the year
Requirements
EditText
6 numbers for authentication Button
Enabled for login
with 6 numbers
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
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
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
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
Activity
view xml
Presenter
Data Binding
MVP with Data Binding
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
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
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
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
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
https://www.pexels.com
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
android:text=“@{@string/title(presenter.year)}”
Expression Language
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
android:textColor=“#fe3dfc”
@InverseMethod
colorText.value = "#000000"
android:textColor=“#fe3dfc”
android:textColor=“@{colorText}”
@InverseMethod
android:textColor=“#fe3dfc”
android:textColor=“@{colorText}” // ERROR!!
@InverseMethod
colorText.value = "#000000"
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
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
android:visibility=“@{title != null}”
visible | invisible | gone
@BindingConversion
object Conversions {
@JvmStatic
@BindingConversion
fun convertBooleanToVisibility(visible: Boolean)
= if (visible) View.VISIBLE else View.GONE
}
android:visibility=“@{title != null}”
visible | invisible | gone
Normally
1. visible | invisible ???
2. visible | gone ???
android:visibility=“@{title != null}”
visible | invisible | gone
Normally
1. visible | invisible
2. visible | gone
app:visible=“@{title != null}”
app:visibleOrGone=“@{title != null}”
@BindingAdapter
@Target(ElementType.METHOD)
public @interface BindingAdapter {
String[] value();
boolean requireAll() default true;
}
@BindingAdapter
@Target(ElementType.METHOD)
public @interface BindingAdapter {
String[] value();
boolean requireAll() default true;
}
// Example
@BindingAdapter("visible")
@BindingAdapter(value={"url", "size"},
requireAll=false)
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
}
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>()
...
}
android:text=“@={presenter.authCode)}”
Two-way data binding
android:text=“@={presenter.authCode)}”
Two-way data binding
OK
presenter.authCode = null
android:text=“@={presenter.authCode)}”
Two-way data binding
OKOK
123456 presenter.authCode = “123456”
android:text=“@={presenter.authCode)}”
Two-way data binding
OK
1234
OK
presenter.authCode = “1234”
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();
}
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
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);
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();
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;
}
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
}
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
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>
class MainPresenter {
var year : String
val authCode = MutableLiveData<String>()
init {
year = "2018"
}
fun login() {
// do something!
}
}
Activity Presenterxml Data Binding
Activity
view xml
Presenter
Data Binding
AS-IS : MVP with Data Binding
class MainViewModel {
var year : String
val authCode = MutableLiveData<String>()
init {
year = "2018"
}
fun login() {
// do something!
}
}
Activity ViewModelxml Data Binding
TO-BE : MVVM with Data Binding
Activity
view xml
ViewModel
Data Binding
How DataBinding works
Seoul
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)}"
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)}"
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?
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
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>
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>
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
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
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
/* 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
/* 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();
}
/* 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
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
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
@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
@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
@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
&
@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
@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
@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
@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
Binding
- Initialized
- set Variables
OK
1
OK
ActivityMainBinding.java
flag vm.authCode : 0x1L
Input 1
1
OK
ActivityMainBinding.java
flag vm.authCode : 0x1L
mDirtyFlags = 0x1L
@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
@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) {
}
}
UI Modularization
Seoul
Android DataBinding (ViewModel, UI Modularization and Testing)
Reusable View
ItemScheduleinding.java
"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
"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" />
Includes
binding.schedule = SpeakerSchedule(“KimKevin”, “Android...”)
<include
android:layout=“@layout/item_schedule”
app:schedule=“@{schedule}” .../>
<variable name="schedule"
type=“com.github.kimkevin…SpeakerSchedule" />
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
}
}
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
"100
Listing & ViewHolder
Listing & ViewHolder
ItemViewHolder
class ItemViewHolder(v: View) : RecyclerView.ViewHolder
Listing & ViewHolder
HeaderViewHolder
ItemViewHolder
class ItemViewHolder(v: View) : RecyclerView.ViewHolder
class HeaderViewHolder(v: View) : RecyclerView.ViewHolder
Listing & ViewHolder
HeaderViewHolder
ItemViewHolder
ProgressViewHolder
class ItemViewHolder(v: View) : RecyclerView.ViewHolder
class HeaderViewHolder(v: View) : RecyclerView.ViewHolder
class ProgressViewHolder(v: View) : RecyclerView.ViewHolder
class BindingViewHolder<T: ViewDataBinding>
constructor(val binding: T): RecyclerView.ViewHolder(binding.root)
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
Unit Testing
Seoul
TextView
Welcome message
for Devfest Seoul of the year
Requirements
EditText
6 numbers for authentication Button
Enabled for login
with 6 numbers
TextView
Welcome message
for Devfest Seoul of the year
Requirements
EditText
6 numbers for authentication Button
Enabled for login
with 6 numbers
Business
UI
<TextView
android:text=“@{@string/title(vm.year)}”
.../>
???
<TextView
android:text=“@{@string/title(vm.year)}”
.../>
Business
UI
???
<TextView
android:text=“@{@string/title(vm.year)}”
.../>
Business
UI
vm.year
<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
<Button
android:enabled=“@{vm.authCode >= 6}”
.../>
Business
UI
<Button
android:enabled=“@{vm.authCode >= 6}”
.../>
???
<Button
android:enabled=“@{vm.authCode >= 6}”
.../>
Business
UI
vm.authCode >= 6
<Button
android:enabled=“@{vm.authCode >= 6}”
.../>
Business
UI
fun String?.isCodeValid()
= this?.length ?: 0 >= 6
<Button
android:enabled=“@{ValidationUtil.isCodeValid(vm.authCode)}”
.../>
Business
UI
<Button
android:enabled=“@{ValidationUtil.isCodeValid(vm.authCode)}”
.../>
@Test
fun validateLessThan6Numbers() {
assertFalse(“1234".isCodeValid())
}
Business
UI
<Button
android:enabled=“@{ValidationUtil.isCodeValid(vm.authCode)}”
.../>
@Test
fun validate6Numbers() {
assertTrue("123456".isCodeValid())
}
Business
UI
Seoul
Thank you
Yongjun Kim, kakaobank
@imkimkevin
Q&A
imkimkevin@gmail.com
https://www.facebook.com/imkimkevin
Busan
1 of 121

More Related Content

Similar to Android DataBinding (ViewModel, UI Modularization and Testing)(20)

Android DataBinding (ViewModel, UI Modularization and Testing)

  • 1. Yongjun Kim, kakaobank @imkimkevin Seoul Android DataBinding UI Modularization, ViewModel and Testing
  • 2. AGENDA Korea ● The reasons to use Data Binding ● How Data Binding works ● UI Modularization ● Unit Testing
  • 5. as Android Developer Activity (Fragment, View) Drawing UI Receiving User Interactions Service Broadcast Receiver Content Provider UI
  • 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
  • 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
  • 8. 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
  • 9. The reasons to use Data Binding Seoul
  • 10. 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) } . . .
  • 11. 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 } . . .
  • 12. So far, It s ok BusinessUI Activity Activity PresenterActivity Presenter Presenter
  • 13. So far, It s ok one to one but testable BusinessUI Activity Activity PresenterActivity Presenter Presenter
  • 14. 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
  • 15. 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
  • 18. 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" }
  • 21. TextView Welcome message for Devfest Seoul of the year Requirements EditText 6 numbers for authentication Button Enabled for login with 6 numbers
  • 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 } Activity
  • 23. 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
  • 24. 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
  • 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 } presenter.year presenter.validate(s.toString()) fun validate(number: String) { view.setButtonEnabled(number.isCodeValid()) } fun setButtonEnabled(enable: Boolean) { button.isEnabled = enable } Activity Presenter
  • 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 } Activity Presenterxml Data Binding
  • 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) Activity Presenterxml Data Binding val binding = DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main) binding.setLifecycleOwner(this) // LiveData
  • 29. 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
  • 30. 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
  • 31. 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
  • 33. 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
  • 35. 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
  • 39. 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
  • 40. 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
  • 41. android:visibility=“@{title != null}” visible | invisible | gone @BindingConversion object Conversions { @JvmStatic @BindingConversion fun convertBooleanToVisibility(visible: Boolean) = if (visible) View.VISIBLE else View.GONE }
  • 42. android:visibility=“@{title != null}” visible | invisible | gone Normally 1. visible | invisible ??? 2. visible | gone ???
  • 43. android:visibility=“@{title != null}” visible | invisible | gone Normally 1. visible | invisible 2. visible | gone app:visible=“@{title != null}” app:visibleOrGone=“@{title != null}”
  • 44. @BindingAdapter @Target(ElementType.METHOD) public @interface BindingAdapter { String[] value(); boolean requireAll() default true; }
  • 45. @BindingAdapter @Target(ElementType.METHOD) public @interface BindingAdapter { String[] value(); boolean requireAll() default true; } // Example @BindingAdapter("visible") @BindingAdapter(value={"url", "size"}, requireAll=false)
  • 46. 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 }
  • 47. 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>() ... }
  • 52. 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(); }
  • 53. 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
  • 54. 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);
  • 55. 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();
  • 56. 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; }
  • 57. 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 }
  • 58. 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
  • 59. 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>
  • 60. class MainPresenter { var year : String val authCode = MutableLiveData<String>() init { year = "2018" } fun login() { // do something! } } Activity Presenterxml Data Binding
  • 62. class MainViewModel { var year : String val authCode = MutableLiveData<String>() init { year = "2018" } fun login() { // do something! } } Activity ViewModelxml Data Binding
  • 63. TO-BE : MVVM with Data Binding Activity view xml ViewModel Data Binding
  • 69. 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>
  • 70. 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>
  • 71. 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
  • 72. 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
  • 73. 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
  • 74. /* 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
  • 75. /* 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(); }
  • 76. /* 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
  • 77. 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
  • 78. 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
  • 79. @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
  • 80. @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
  • 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); } } mDirtyFlags = 0x4L 1 1 0 1 1 1 0x6L 0x7L (0x6L & 0x7L) != 0 &
  • 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 0 0x6L 0x7L & 1 1 0
  • 83. @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
  • 84. @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
  • 85. @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
  • 89. @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
  • 90. @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) { } }
  • 95. "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
  • 97. Includes binding.schedule = SpeakerSchedule(“KimKevin”, “Android...”) <include android:layout=“@layout/item_schedule” app:schedule=“@{schedule}” .../> <variable name="schedule" type=“com.github.kimkevin…SpeakerSchedule" />
  • 98. 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 } }
  • 99. 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
  • 101. Listing & ViewHolder ItemViewHolder class ItemViewHolder(v: View) : RecyclerView.ViewHolder
  • 102. Listing & ViewHolder HeaderViewHolder ItemViewHolder class ItemViewHolder(v: View) : RecyclerView.ViewHolder class HeaderViewHolder(v: View) : RecyclerView.ViewHolder
  • 103. Listing & ViewHolder HeaderViewHolder ItemViewHolder ProgressViewHolder class ItemViewHolder(v: View) : RecyclerView.ViewHolder class HeaderViewHolder(v: View) : RecyclerView.ViewHolder class ProgressViewHolder(v: View) : RecyclerView.ViewHolder
  • 104. class BindingViewHolder<T: ViewDataBinding> constructor(val binding: T): RecyclerView.ViewHolder(binding.root)
  • 105. 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
  • 107. TextView Welcome message for Devfest Seoul of the year Requirements EditText 6 numbers for authentication Button Enabled for login with 6 numbers
  • 108. TextView Welcome message for Devfest Seoul of the year Requirements EditText 6 numbers for authentication Button Enabled for login with 6 numbers Business UI
  • 112. <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
  • 117. fun String?.isCodeValid() = this?.length ?: 0 >= 6 <Button android:enabled=“@{ValidationUtil.isCodeValid(vm.authCode)}” .../> Business UI
  • 120. Seoul Thank you Yongjun Kim, kakaobank @imkimkevin