본 포스팅은 DroidKaigi 2018 ~ DataBindingのコードを読む 을 기본으로 번역하여 작성했습니다
제 일본어 실력으로 인하여 오역이나 오타가 발생할 수 있습니다.
DataBinding 코드 읽기
※ Android Gradle Plugin 3.0을 대상으로 합니다
Gradle Plugin
의존 라이브러리 추가
https://android.googlesource.com/platform/frameworks/data-binding/+/gradle_3.0.0/extensions/library/
https://android.googlesource.com/platform/frameworks/data-binding/+/gradle_3.0.0/baseLibrary/
https://android.googlesource.com/platform/frameworks/data-binding/+/gradle_3.0.0/compiler/
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));
// TODO load config name from source sets
project.getDependencies()
.add(
"annotationProcessor",
SdkConstants.DATA_BINDING_ANNOTATION_PROCESSOR_ARTIFACT + ":" + version);
if (options.isEnabledForTests() || this instanceof LibraryTaskManager) {
project.getDependencies().add("androidTestAnnotationProcessor",
SdkConstants.DATA_BINDING_ANNOTATION_PROCESSOR_ARTIFACT + ":" +
version);
}
if (options.getAddDefaultAdapters()) {
project.getDependencies()
.add(
"api",
SdkConstants.DATA_BINDING_ADAPTER_LIB_ARTIFACT
+ ":"
+ dataBindingBuilder.getBaseAdaptersVersion(version));
}
}
Task 추가
@BindingBuildInfo(buildId="8c129f13-798b-4137-8327-f76a34f6a51f")
public class DataBindingInfo {}
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);
}
}
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);
}
}
레이아웃 파일 분석
<?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>
<?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"> <!-- View -->
<Expressions>
<Expression attribute="android:text" text="viewModel.text"> <!-- 식 -->
<Location endLine="20" endOffset="43" startLine="20" startOffset="12" />
<TwoWay>false</TwoWay> <!-- Two-way binding -->
<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>
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, ndIndex);
Location attrLocation = new Location(attr);
Location valueLocation = new Location();
// offset to 0 based
valueLocation.startLine = attr.attrValue.getLine() - 1;
valueLocation.startOffset = ttr.attrValue.getCharPositionInLine() + ttr.attrValue.getText().indexOf(strippedValue);
valueLocation.endLine = attrLocation.endLine;
valueLocation.endOffset = attrLocation.endOffset - 2; // ccount for: "}
bindingTargetBundle.addBinding(escapeQuotesattr.attrName.getText(), false), strippedValue, isTwoWay, ttrLocation, valueLocation);
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);
Gradle Plugin 추가 내용
dataBinding {
enabled true
version "2.3.3"
addDefaultAdapters false
enabledForTests true
}
Annotation Processor
kotlin이 사용되고 있다
@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"} // DataBindingInfo Annotation
)
/**
* 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;
}
if (mCompilerArgs.isTestVariant() && !mCompilerArgs.isEnabledForTests() &&
Binding 클래스 생성
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;
}
requestRebind();
<?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>
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)
nl(readWithDependants(readNow, justRead, batch, tmpDirtyFlags))
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
| left=expression op=('==' | '!=') right=expressi
BR 클래스 생성
public class BR {
public static final int _all = 0;
public static final int text = 1;
public static final int viewModel = 2;
}
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()
}
}
DataBinderMapper 클래스 생성
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;
}
// …
}
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("")
block("public $className()") {
코드 생성 및 실행
notifyPropertyChanged
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);
}
}
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);
// ...
}
}
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();
}
}
[ViewModel (참조)] → [WeakListener (참조)] → ActivityMainBinding(ViewDataBinding)
[ViewModel notifyProperty Changed → (참조)] → [WeakListener (참조)] → ActivityMainBinding(ViewDataBinding) → View 갱신 (executeBindings)
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);
}
}
// …
}
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);
}
}
}
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" />
@BindingMethods({
@BindingMethod(type = View.class,
attribute = "android:onClick",
method = "setOnClickListener"),
})
public class ViewBindingAdapter {
}
public class ActivityMainBinding extends ViewDataBinding {
@Override
protected void executeBindings() {
OnClickListener listener = null;
listener = new OnClickListenerImpl();
listener.setValue(viewModel);
button.setOnClickListener(listener); // EventListener를 설정
}
public static class OnClickListenerImpl implements OnClickListener{ // 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에 정의한 메소드
}
}
}
s@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으로 정의한 메소드
// ...
}
}
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}"/>
package android.databinding;
public interface DataBindingComponent {
SampleBindingAdapter getSampleBindingAdapter();
}
public class MyComponent implements DataBindingComponent {
@Override
public SampleBindingAdapter getSampleBindingAdapter() {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
return new SampleBindingAdapter(format);
}
}
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()); // 정의한 Component를 지정
// 기본을 지정하고 싶은 경우
DataBindingUtil.setDefaultComponent(new MyComponent());
// ...
}
}
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
}
}
Two-way binding
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={viewModel.text}"/>
@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);
}
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(attribute = "android:text",
event = "android:textAttrChanged")
public static String getTextString(TextView view) {
return view.getText().toString();
}
public class ActivityMainBinding extends ViewDataBinding {
private InverseBindingListener textAttrChanged = new InverseBindingListener() {
@Override
public void onChange() {
String callbackArg_0 = TextViewBindingAdapter.getTextString(editText); // @InverseBindingAdapter
// ...
viewModel.setText(callbackArg_0)); // ViewModel에 설정
}
};
@Override
protected void executeBindings() {
// …
TextViewBindingAdapter.setText(editText, viewModelText);
TextViewBindingAdapter.setTextWatcher(editText, null, null, null, textAttrChanged);
}
}
@BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
"android:afterTextChanged", "android:textAttrChanged"},
requireAll = false)
public static void setTextWatcher(TextView view,
final BeforeTextChanged before,
final OnTextChanged on,
final AfterTextChanged after,
final InverseBindingListener textAttrChanged) {
// ...
newValue = new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (textAttrChanged != null) {
textAttrChanged.onChange();
}
}
// ...
};
// ...
}
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:textAttrChanged")
}
}
정리
comments powered by Disqus
Subscribe to this blog via RSS.
LazyColumn/Row에서 동일한 Key를 사용하면 크래시가 발생하는 이유
Posted on 30 Nov 2024