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

DataBindingのコードを読む - DroidKaigi 2018

star_zero
February 06, 2018

DataBindingのコードを読む - DroidKaigi 2018

DroidKaigi 2018

star_zero

February 06, 2018
Tweet

More Decks by star_zero

Other Decks in Programming

Transcript

  1. 話すこと • DataBindingのコードから仕組みを解説 • Gradle Plugin • Annotation Processor •

    コード生成・実行 ※Android Gradle Plugin 3.0を対象してます
  2. • https://android.googlesource.com/platform/tools/base/+/gradle_3.0.0/build-system/gradle- core/src/main/java/com/android/build/gradle/internal/TaskManager.java 依存ライブラリの追加 public abstract class TaskManager { //

    ... public void addDataBindingDependenciesIfNecessary(DataBindingOptions options) { if (!options.isEnabled()) { return; } String version = MoreObjects.firstNonNull(options.getVersion(), dataBindingBuilder.getCompilerVersion()); project.getDependencies() .add( "api", SdkConstants.DATA_BINDING_LIB_ARTIFACT + ":" + dataBindingBuilder.getLibraryVersion(version)); project.getDependencies() .add( "api", SdkConstants.DATA_BINDING_BASELIB_ARTIFACT + ":" + dataBindingBuilder.getBaseLibraryVersion(version)); project.getDependencies() .add( "annotationProcessor", SdkConstants.DATA_BINDING_ANNOTATION_PROCESSOR_ARTIFACT + ":" + version); if (options.isEnabledForTests() || this instanceof LibraryTaskManager) { project.getDependencies().add("androidTestAnnotationProcessor",
  3. タスク追加 • https://android.googlesource.com/platform/tools/base/+/gradle_3.0.0/build-system/gradle- core/src/main/java/com/android/build/gradle/internal/TaskManager.java public abstract class TaskManager { //

    ... protected void createDataBindingTasksIfNecessary(@NonNull TaskFactory tasks, @NonNull VariantScope scope) { if (!extension.getDataBinding().isEnabled()) { return; } VariantType type = scope.getVariantData().getType(); boolean isTest = type == VariantType.ANDROID_TEST || type == VariantType.UNIT_TEST; if (isTest && !extension.getDataBinding().isEnabledForTests()) { BaseVariantData testedVariantData = scope.getTestedVariantData(); if (testedVariantData.getType() != LIBRARY) { return; } } dataBindingBuilder.setDebugLogEnabled(getLogger().isDebugEnabled()); AndroidTask<DataBindingExportBuildInfoTask> exportBuildInfo = androidTasks .create(tasks, new DataBindingExportBuildInfoTask.ConfigAction(scope)); exportBuildInfo.dependsOn(tasks, scope.getMergeResourcesTask()); exportBuildInfo.dependsOn(tasks, scope.getSourceGenTask()); scope.setDataBindingExportBuildInfoTask(exportBuildInfo); } }
  4. タスク追加 • https://android.googlesource.com/platform/frameworks/data-binding/+/gradle_3.0.0/ compilerCommon/src/main/java/android/databinding/tool/LayoutXmlProcessor.java public class LayoutXmlProcessor { // ...

    public void writeEmptyInfoClass() { final Class annotation = BindingBuildInfo.class; String classString = "package " + RESOURCE_BUNDLE_PACKAGE + ";\n\n" + "import " + annotation.getCanonicalName() + ";\n\n" + "@" + annotation.getSimpleName() + "(buildId=\"" + mBuildId + "\")\n" + "public class " + CLASS_NAME + " {}\n"; mFileWriter.writeToFile(RESOURCE_BUNDLE_PACKAGE + "." + CLASS_NAME, classString); } }
  5. レイアウトファイルのパース • mergeDebugResourceタスクで実行される • レイアウトファイルの <layout> タグあるもの をパースして、新しくXMLファイルを生成する <?xml version="1.0"

    encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <!-- ... --> </layout>
  6. レイアウトファイルのパース <?xml version="1.0" encoding="utf-8" standalone="yes"?> <Layout absoluteFilePath="/path_to/project/app/src/main/res/layout/activity_main.xml" directory="layout" isMerge="false" layout="activity_main"

    modulePackage="com.star_zero.debugdatabinding"> <Variables name="viewModel" declared="true" type="com.star_zero.debugdatabinding.ViewModel"> <location endLine="9" endOffset="61" startLine="7" startOffset="8" /> </Variables> <Targets> <Target tag="layout/activity_main_0" view="android.support.constraint.ConstraintLayout"> <Expressions /> <location endLine="38" endOffset="49" startLine="12" startOffset="4" /> </Target> <Target id="@+id/textView" tag="binding_1" view="TextView"> <Expressions> <Expression attribute="android:text" text="viewModel.text"> <Location endLine="20" endOffset="43" startLine="20" startOffset="12" /> <TwoWay>false</TwoWay> <ValueLocation endLine="20" endOffset="41" startLine="20" startOffset="28" /> </Expression> </Expressions> <location endLine="24" endOffset="55" startLine="16" startOffset="8" /> </Target> <Target id="@+id/button" view="Button"> <Expressions /> <location endLine="36" endOffset="65" startLine="26" startOffset="8" /> </Target> </Targets> </Layout> 変数 View 式 Two-way binding
  7. レイアウトファイルのパース • https://android.googlesource.com/platform/frameworks/data-binding/+/gradle_3.0.0/ compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java public class LayoutFileParser { // ...

    private void parseExpressions(String newTag, final XMLParser.ElementContext rootView, final boolean isMerge, ResourceBundle.LayoutFileBundle bundle) { // ... for (XMLParser.ElementContext parent : bindingElements) { // ... for (XMLParser.AttributeContext attr : XmlEditor.expressionAttributes(parent)) { String value = escapeQuotes(attr.attrValue.getText(), true); final boolean isOneWay = value.startsWith("@{"); final boolean isTwoWay = value.startsWith("@={"); if (isOneWay || isTwoWay) { if (value.charAt(value.length() - 1) != '}') { L.e("Expecting '}' in expression '%s'", attr.attrValue.getText()); } final int startIndex = isTwoWay ? 3 : 2; final int endIndex = value.length() - 1; final String strippedValue = value.substring(startIndex, endIndex); Location attrLocation = new Location(attr); Location valueLocation = new Location(); // offset to 0 based valueLocation.startLine = attr.attrValue.getLine() - 1; valueLocation.startOffset = attr.attrValue.getCharPositionInLine() + attr.attrValue.getText().indexOf(strippedValue); valueLocation.endLine = attrLocation.endLine; valueLocation.endOffset = attrLocation.endOffset - 2; // account for: "} bindingTargetBundle.addBinding(escapeQuotes(attr.attrName.getText(), false),
  8. レイアウトファイルのパース • https://android.googlesource.com/platform/frameworks/data-binding/+/gradle_3.0.0/ compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java public class LayoutFileParser { // ...

    private void parseData(File xml, XMLParser.ElementContext data, ResourceBundle.LayoutFileBundle bundle) { if (data == null) { return; } for (XMLParser.ElementContext imp : filter(data, "import")) { final Map<String, String> attrMap = attributeMap(imp); String type = attrMap.get("type"); String alias = attrMap.get("alias"); Preconditions.check(StringUtils.isNotBlank(type), "Type of an import cannot be empty." + " %s in %s", imp.toStringTree(), xml); if (Strings.isNullOrEmpty(alias)) { alias = type.substring(type.lastIndexOf('.') + 1); } bundle.addImport(alias, type, new Location(imp)); } for (XMLParser.ElementContext variable : filter(data, "variable")) { final Map<String, String> attrMap = attributeMap(variable); String type = attrMap.get("type"); String name = attrMap.get("name"); Preconditions.checkNotNull(type, "variable must have a type definition %s in %s", variable.toStringTree(), xml); Preconditions.checkNotNull(name, "variable must have a name %s in %s", variable.toStringTree(), xml); bundle.addVariable(name, type, new Location(variable), true);
  9. Annotation Processor • https://android.googlesource.com/platform/frameworks/data-binding/+/gradle_3.0.0/ compiler/src/main/java/android/databinding/annotationprocessor/ProcessDataBinding.java @SupportedAnnotationTypes({ "android.databinding.BindingAdapter", "android.databinding.InverseBindingMethods", "android.databinding.InverseBindingAdapter", "android.databinding.InverseMethod",

    "android.databinding.Untaggable", "android.databinding.BindingMethods", "android.databinding.BindingConversion", "android.databinding.BindingBuildInfo"} ) /** * Parent annotation processor that dispatches sub steps to ensure execution order. * Use initProcessingSteps to add a new step. */ public class ProcessDataBinding extends AbstractProcessor { private List<ProcessingStep> mProcessingSteps; private DataBindingCompilerArgs mCompilerArgs; @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { if (mProcessingSteps == null) { readArguments(); initProcessingSteps(); } if (mCompilerArgs == null) { return false; } DataBindingInfo のアノテーション
  10. public class ActivityMainBinding extends ViewDataBinding { @Nullable private static final

    IncludedLayouts sIncludes; @Nullable private static final SparseIntArray sViewsWithIds; static { sIncludes = null; sViewsWithIds = null; } @NonNull private final LinearLayout mboundView0; @NonNull public final TextView textView1; @Nullable private ViewModel mViewModel; public ActivityMainBinding(@NonNull DataBindingComponent bindingComponent, @NonNull View root) { super(bindingComponent, root, 2); final Object[] bindings = mapBindings(bindingComponent, root, 2, sIncludes, sViewsWithIds); this.mboundView0 = (LinearLayout) bindings[0]; this.mboundView0.setTag(null); this.textView1 = (TextView) bindings[1]; this.textView1.setTag(null); setRootTag(root); // listeners invalidateAll(); } @Override public void invalidateAll() { synchronized(this) { mDirtyFlags = 0x4L; } Bindingクラス生成
  11. Bindingクラス生成 • レイアウトファイルをパースしたXMLファイル
 からBindingクラスを生成 <?xml version="1.0" encoding="utf-8" standalone="yes"?> <Layout absoluteFilePath="/path_to/project/app/src/main/res/layout/

    activity_main.xml" directory="layout" isMerge="false" layout="activity_main" modulePackage="com.star_zero.debugdatabinding"> <Variables name="viewModel" declared="true" type="com.star_zero.debugdatabinding.ViewModel"> <location endLine="9" endOffset="61" startLine="7" startOffset="8" /> </Variables> <Targets> <Target tag="layout/activity_main_0" view="android.support.constraint.ConstraintLayout"> <Expressions /> <location endLine="38" endOffset="49" startLine="12" startOffset="4" /> </Target> <!— … —> </Targets> </Layout>
  12. Bindingクラス生成 • import文の生成 • <variable>からプロパティ生成 • android:idがあるViewのフィールド作成 • @{}の式をパース •

    式のパースにはANTLR v4が使われている • EventListenerの実装 • Viewに反映する処理 • BindingAdapterアノテーション • set + XMLの属性名 のメソッド
  13. Bindingクラス生成 • https://android.googlesource.com/platform/frameworks/data-binding/+/gradle_3.0.0/ compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt class LayoutBinderWriter(val layoutBinder : LayoutBinder) {

    // ... fun executePendingBindings() = kcode("") { nl("@Override") block("protected void executeBindings()") { val tmpDirtyFlags = FlagSet(mDirtyFlags.buckets) tmpDirtyFlags.localName = "dirtyFlags"; for (i in (0..mDirtyFlags.buckets.size - 1)) { nl("${tmpDirtyFlags.type} ${tmpDirtyFlags.localValue(i)} = 0;") } block("synchronized(this)") { for (i in (0..mDirtyFlags.buckets.size - 1)) { nl("${tmpDirtyFlags.localValue(i)} = ${mDirtyFlags.localValue(i)};") nl("${mDirtyFlags.localValue(i)} = 0;") } } model.pendingExpressions.filter { it.needsLocalField }.forEach { nl("${it.resolvedType.toJavaCode()} ${it.executePendingLocalName} = ${if (it.isVariable()) it.fieldName else it.defaultValue};") } L.d("writing executePendingBindings for %s", className) do { val batch = ExprModel.filterShouldRead(model.pendingExpressions) val justRead = arrayListOf<Expr>() L.d("batch: %s", batch) while (!batch.none()) { val readNow = batch.filter { it.shouldReadNow(justRead) } if (readNow.isEmpty()) { throw IllegalStateException("do not know what I can read. bailing out $ {batch.joinToString("\n")}") } L.d("new read now. batch size: %d, readNow size: %d", batch.size, readNow.size)
  14. Bindingクラス生成 • https://android.googlesource.com/platform/frameworks/data-binding/+/gradle_3.0.0/ compilerCommon/BindingExpression.g4 grammar BindingExpression; // ... expression :

    '(' expression ')' # Grouping // this isn't allowed yet. // | THIS # Primary | literal # Primary | VoidLiteral # Primary | identifier # Primary | classExtraction # Primary | resources # Resource // | typeArguments (explicitGenericInvocationSuffix | 'this' arguments) # GenericCall | expression '.' Identifier # DotOp | expression '::' Identifier # FunctionRef // | expression '.' 'this' # ThisReference // | expression '.' explicitGenericInvocation # ExplicitGenericInvocationOp | expression '[' expression ']' # BracketOp | target=expression '.' methodName=Identifier '(' args=expressionList? ')' # MethodInvocation | methodName=Identifier '(' args=expressionList? ')' # GlobalMethodInvocation | '(' type ')' expression # CastOp | op=('+'|'-') expression # UnaryOp | op=('~'|'!') expression # UnaryOp | left=expression op=('*'|'/'|'%') right=expression # MathOp | left=expression op=('+'|'-') right=expression # MathOp | left=expression op=('<<' | '>>>' | '>>') right=expression # BitShiftOp | left=expression op=('<=' | '>=' | '>' | '<') right=expression # ComparisonOp | expression 'instanceof' type # InstanceOfOp
  15. BRクラス生成 • @BindableからBRクラスを生成する • notifyPropertyChangedで使う public class BR { public

    static final int _all = 0; public static final int text = 1; public static final int viewModel = 2; }
  16. BRクラス生成 • https://android.googlesource.com/platform/frameworks/data-binding/+/gradle_3.0.0/ compiler/src/main/kotlin/android/databinding/tool/writer/BRWriter.kt class BRWriter(properties: Set<String>, val useFinal :

    Boolean) { val indexedProps = properties.sorted().withIndex() fun write(pkg : String): String = "package $pkg;${StringUtils.LINE_SEPARATOR}$klass" val klass: String by lazy { kcode("") { val prefix = if (useFinal) "final " else ""; annotateWithGenerated() block("public class BR") { tab("public static ${prefix}int _all = 0;") indexedProps.forEach { tab ("public static ${prefix}int ${it.value} = ${it.index + 1};") } } }.generate() } }
  17. DataBinderMapperクラス生成 • レイアウトとBindingクラスのマッピング class DataBinderMapper { final static int TARGET_MIN_SDK

    = 19; public DataBinderMapper() { } public ViewDataBinding getDataBinder(DataBindingComponent bindingComponent, View view, int layoutId) { switch(layoutId) { case R.layout.activity_main: return ActivityMainBinding.bind(view, bindingComponent); } return null; } // … }
  18. DataBinderMapperクラス生成 • https://android.googlesource.com/platform/frameworks/data-binding/+/gradle_3.0.0/ compiler/src/main/kotlin/android/databinding/tool/writer/BindingMapperWriter.kt class BindingMapperWriter(var pkg : String, var

    className: String, val layoutBinders : List<LayoutBinder>, val compilerArgs: DataBindingCompilerArgs) { // ... fun write(brWriter : BRWriter) = kcode("") { nl("package $pkg;") nl("import ${compilerArgs.modulePackage}.BR;") val extends = if (generateAsTest) "extends $appClassName" else "" annotateWithGenerated() block("class $className $extends") { nl("final static int TARGET_MIN_SDK = ${compilerArgs.minApi};") if (generateTestOverride) { nl("static $appClassName mTestOverride;") block("static") { block("try") { nl("mTestOverride = ($appClassName) $appClassName.class.getClassLoader().loadClass(\"$pkg.$testClassName\").newInstance();") } block("catch(Throwable ignored)") { nl("// ignore, we are not running in test mode") nl("mTestOverride = null;") } } } nl("")
  19. notifyPropertyChanged • Viewへの変更通知を行う public class ViewModel extends BaseObservable { private

    String text; @Bindable public String getText() { return text; } public void setText(String text) { this.text = text; notifyPropertyChanged(BR.text); } }
  20. • Activity notifyPropertyChanged public class MainActivity extends AppCompatActivity { private

    ActivityMainBinding binding; private ViewModel viewModel = new ViewModel(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView( this, R.layout.activity_main); binding.setViewModel(viewModel); // ... } }
  21. • Bindingクラス public class ActivityMainBinding extends ViewDataBinding { // ...

    public void setViewModel(@Nullable ViewModel ViewModel) { updateRegistration(2, ViewModel); this.mViewModel = ViewModel; synchronized(this) { mDirtyFlags |= 0x4L; } notifyPropertyChanged(BR.viewModel); super.requestRebind(); } } notifyPropertyChanged 通知できる状態をつくる
  22. executeBindings public class ActivityMainBinding extends ViewDataBinding { // … @Override

    protected void executeBindings() { long dirtyFlags = 0; synchronized(this) { dirtyFlags = mDirtyFlags; mDirtyFlags = 0; } java.lang.String viewModelText = null; ViewModel viewModel = mViewModel; if ((dirtyFlags & 0x7L) != 0) { if (viewModel != null) { viewModelText = viewModel.getText(); } } if ((dirtyFlags & 0x7L) != 0) { TextViewBindingAdapter.setText(this.textView, viewModelText); } } // … }
  23. mDirtyFlags • プロパティの変更ビットフラグ public class ActivityMainBinding extends ViewDataBinding { @Override

    public void invalidateAll() { synchronized(this) { mDirtyFlags = 0x10L; } requestRebind(); } private boolean onChangeViewModelText1(ObservableField<String> ViewModelText1, int fieldId) { if (fieldId == BR._all) { synchronized(this) { mDirtyFlags |= 0x1L; } return true; } return false; } @Override protected void executeBindings() { // ... if ((dirtyFlags & 0x19L) != 0) { TextViewBindingAdapter.setText(this.textView1, viewModelText1Get); } } }
  24. mDirtyFlags • longのビットフラグ • プロパティが64を超える場合は新しい変数が
 作られる • mDirtyFlags_1 • mDirtyFlags_2

    • 1番左のビットはすべて変更フラグ • 変数が複数ある場合は最後の変数の1番左
  25. EventListener public class ViewModel { public void onClick(View view) {

    // ... } } <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="@{viewModel::onClick}" android:text="Button" />
  26. EventListener • Bindingクラス public class ActivityMainBinding extends ViewDataBinding { @Override

    protected void executeBindings() { OnClickListener listener = null; listener = new OnClickListenerImpl(); listener.setValue(viewModel); button.setOnClickListener(listener); } public static class OnClickListenerImpl implements OnClickListener{ private ViewModel value; public OnClickListenerImpl setValue(ViewModel value) { this.value = value; return value == null ? null : this; } @Override public void onClick(android.view.View arg0) { this.value.onClick(arg0); } } } ViewModelに定義したメソッド EventListenerをセット OnClickListenerを定義
  27. • カスタムセッター • アノテーションで指定したメソッドが使われる @BindingAdapter public class CustomBinding { @BindingAdapter("intValue")

    public static void setIntValue(TextView view, int value) { view.setText(String.valueOf(value)); } } public class ActivityMainBinding extends ViewDataBinding { @Override protected void executeBindings() { // … CustomBinding.setIntValue(this.textView, viewModelValueGet); // ... } } @BindingAdapterで定義したメソッド
  28. @BindingAdapter • インスタンスメソッドも使える public class SampleBindingAdapter { private SimpleDateFormat format;

    public SampleBindingAdapter(SimpleDateFormat format) { this.format = format; } @BindingAdapter("dateValue") public void setDate(TextView view, Date date) { view.setText(format.format(date)); } } インスタンスメソッド <TextView android:id="@+id/text" android:layout_width="match_parent" android:layout_height="wrap_content" app:dateValue="@{viewModel.date}"/>
  29. @BindingAdapter • DataBindingComponentを実装する public class MyComponent implements DataBindingComponent { @Override

    public SampleBindingAdapter getSampleBindingAdapter() { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); return new SampleBindingAdapter(format); } }
  30. @BindingAdapter • DataBindingUtil.setContentViewに設定 • もしくはsetDefaultComponentでデフォルトを 設定 public class MainActivity extends

    AppCompatActivity { private ActivityMainBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView( this, R.layout.activity_main, new MyComponent()); // デフォルトを指定したい場合 DataBindingUtil.setDefaultComponent(new MyComponent()); // ... } } 定義したComponentを指定
  31. • Bindingクラス @BindingAdapter public class ActivityMainBinding extends ViewDataBinding { public

    ActivityMainBinding( @NonNull DataBindingComponent bindingComponent, @NonNull View root) { super(bindingComponent, root, 2); // ... } @Override protected void executeBindings() { // ... this.mBindingComponent.getSampleBindingAdapter() .setDate(this.textView, viewModelValueGet); } } setContentViewの 引数に渡したComponent
  32. Two-way binding • @={} でTwo-way binding • TextViewのはTextViewBindingAdapterで定義 • https://android.googlesource.com/platform/frameworks/data-

    binding/+/gradle_3.0.0/extensions/baseAdapters/ <EditText android:id="@+id/editText" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@={viewModel.text}"/>
  33. • TextViewBindingAdapter Two-way binding @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; } // ... view.setText(text); }
  34. Two-way binding public class ActivityMainBinding extends ViewDataBinding { private InverseBindingListener

    textAttrChanged = new InverseBindingListener() { @Override public void onChange() { String callbackArg_0 = TextViewBindingAdapter.getTextString(editText); // ... viewModel.setText(callbackArg_0)); } }; @Override protected void executeBindings() { // … TextViewBindingAdapter.setText(editText, viewModelText); TextViewBindingAdapter.setTextWatcher(editText, null, null, null, textAttrChanged); } } @BindingAdapter("android:text") • Bindingクラス
  35. • TextViewBindingAdapter Two-way binding @InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")

    public static String getTextString(TextView view) { return view.getText().toString(); }
  36. Two-way binding • Bindingクラス public class ActivityMainBinding extends ViewDataBinding {

    private InverseBindingListener textAttrChanged = new InverseBindingListener() { @Override public void onChange() { String callbackArg_0 = TextViewBindingAdapter.getTextString(editText); // ... viewModel.setText(callbackArg_0)); } }; @Override protected void executeBindings() { // … TextViewBindingAdapter.setText(editText, viewModelText); TextViewBindingAdapter.setTextWatcher(editText, null, null, null, textAttrChanged); } } @InverseBindingAdapter ViewModelに設定
  37. • TextViewBindingAdapter Two-way binding @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) { // ... newValue = new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (textAttrChanged != null) { textAttrChanged.onChange(); } } // ... }; // ... } @InverseBindingAdapter のevent InverseBindingListener 値を反映したいタイミングでonChange
  38. Two-way binding • Bindingクラス public class ActivityMainBinding extends ViewDataBinding {

    private InverseBindingListener textAttrChanged = new InverseBindingListener() { @Override public void onChange() { String callbackArg_0 = TextViewBindingAdapter.getTextString(editText); // ... viewModel.setText(callbackArg_0)); } }; @Override protected void executeBindings() { // … TextViewBindingAdapter.setText(editText, viewModelText); TextViewBindingAdapter.setTextWatcher(editText, null, null, null, textAttrChanged); } } @BindingAdapter("android:text") @BindingAdapter("android:textAttrChanged")