Data Binding — Fundamentals, Two-Way Binding in Android

Author: IT Sectr Published: 2026-02-20 Reading time: 8 min

Data Binding — an Android Jetpack library that binds UI components from XML layouts to data sources in application code through declarative syntax. We explain the basics: Data Binding eliminates the boilerplate findViewById() code and automatically updates the UI when data changes. According to Google (Android Developers, 2025), Data Binding is used in 45% of Android projects, and in combination with LiveData or StateFlow it provides fully reactive binding without manual subscription management.

Key Takeaways

  • Data Binding — a Jetpack library for declarative binding of XML layouts to data sources using layout tags.
  • Two-way binding — automatic data synchronization between View and ViewModel via @={} in XML.
  • @BindingAdapter — a custom setter for View attributes that extends standard Data Binding.
  • Data Binding + LiveData — reactivity without Observer: LiveData changes automatically update the UI.
  • Data Binding generates a Binding class (e.g., ActivityMainBinding) at compile time, providing type-safe access to Views.

What is Data Binding in Android?

Data Binding is a support library (Android Jetpack) that first appeared in 2015 at Google I/O and was stabilized in Android Gradle Plugin 1.5. It allows binding UI components in XML to data sources (POJO, ViewModel, LiveData) directly in the layout, without calling findViewById() in Activity or Fragment code.

How it works: the XML layout is wrapped in a <layout> tag, which declares a <variable> with a data type. Inside the layout, data is substituted using expressions in curly braces @{}. At compile time, the Android Gradle Plugin generates a Binding class (e.g., ActivityMainBinding) containing direct references to Views with correct types and methods for setting data.

According to the Android Developers Survey (2024), Data Binding reduces the amount of UI code in Activity/Fragment by 30–50% by moving binding logic into XML. The number of errors related to incorrect View types (ClassCastException with findViewById()) drops to zero, since all types are checked at compile time.

Data Binding vs ViewBinding: Which to Choose

ViewBinding is a lighter alternative to Data Binding, introduced in Android Studio 3.6 (2020). ViewBinding generates a Binding class for each layout file, but without support for expressions, variables, and reactivity. Comparison by key criteria:

CriterionData BindingViewBinding
Binding class generationYesYes
XML expressions (@{})YesNo
Two-way bindingYesNo
Reactive (LiveData)YesNo
@BindingAdapterYesNo
Compilation speedSlower (expression processing)Faster
ComplexityHighLow

Google's recommendation (Android Developers, 2025): for most projects ViewBinding is sufficient — it provides type-safe access to Views without the overhead of Data Binding. Choose Data Binding if you need: (1) reactive binding with LiveData/StateFlow from XML, (2) two-way binding for forms, (3) BindingAdapter for custom attributes, (4) XML expressions for formatting. At IT Sectr, we use ViewBinding for simple screens and Data Binding for complex forms and dashboards.

Two-Way Binding: @{} and @={}

One-way binding (@{}) passes data from the source (ViewModel) to the View. Two-way binding (@={}) synchronizes data in both directions: changes in the View (text input, Switch toggle) automatically update the source.

xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="viewModel"
            type="com.example.app.LoginViewModel" />
    </data>

    <LinearLayout ...>
        <!-- One-way: data from ViewModel to TextView -->
        <TextView
            android:text="@{viewModel.userName}" />

        <!-- Two-way: EditText changes → ViewModel, ViewModel → EditText -->
        <EditText
            android:text="@{=viewModel.email}" />

        <CheckBox
            android:checked="@{=viewModel.agreeToTerms}" />
    </LinearLayout>
</layout>

For two-way binding, the ViewModel must use ObservableField, LiveData, or StateFlow. When data changes through user input, Data Binding automatically calls the source's setter. Important: two-way binding works with attributes that have a defined @InverseBindingAdapter. Android provides built-in adapters for: text, checked, visibility, progress, rating, and other standard attributes.

@BindingAdapter: Custom Attributes and Converters

@BindingAdapter is an annotation for Kotlin extension functions that allows defining custom binding logic for any View attribute. For example, loading an image via Glide when specifying a URL in XML, or formatting a date when binding to a TextView.

kotlin
// BindingAdapter for loading image by URL
@BindingAdapter("imageUrl")
fun ImageView.setImageUrl(url: String?) {
    Glide.with(this.context)
        .load(url)
        .placeholder(R.drawable.placeholder)
        .error(R.drawable.error)
        .into(this)
}

// BindingAdapter with multiple attributes
@BindingAdapter("visibleGone")
fun View.setVisibleGone(visible: Boolean) {
    visibility = if (visible) View.VISIBLE else View.GONE
}

// BindingAdapter with converter (formatDate)
@BindingAdapter("formattedDate")
fun TextView.setFormattedDate(timestamp: Long) {
    text = SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()).format(Date(timestamp))
}
xml
<!-- Using BindingAdapter in XML -->
<ImageView
    imageUrl="@{user.avatarUrl}"
    android:layout_width="48dp"
    android:layout_height="48dp" />

<TextView
    formattedDate="@{message.createdAt}"
    visibleGone="@{message.isVisible}" />

@BindingAdapter can accept multiple attributes (requireAll = true/false), allowing value combinations. For example, @BindingAdapter("imageUrl", "circleCrop") — if circleCrop is true, Glide applies the CircleCrop transform. According to Google (Android Performance, 2024), BindingAdapter with Glide in Data Binding processes up to 60 frames per second when scrolling a RecyclerView, since asynchronous loading does not block the UI thread.

Data Binding with LiveData and StateFlow

Data Binding natively supports LiveData since Android Architecture Components 1.0. If a variable in the layout has the LiveData type, Binding automatically subscribes to it and updates the UI when the value changes. For proper operation, you must set the LifecycleOwner in the Binding class: binding.lifecycleOwner = viewLifecycleOwner.

kotlin
// ViewModel with LiveData
class WeatherViewModel : ViewModel() {
    private val _temperature = MutableLiveData("--")
    val temperature: LiveData<String> get() = _temperature

    val cityName = MutableLiveData("Moscow")
    val weatherIcon = MutableLiveData(R.drawable.ic_sunny)

    fun refresh() {
        viewModelScope.launch {
            _temperature.value = weatherRepository.getTemperature()
        }
    }
}

// In Fragment:
val binding = FragmentWeatherBinding.inflate(inflater, container, false)
binding.viewModel = weatherViewModel
binding.lifecycleOwner = viewLifecycleOwner  // ← required for LiveData
xml
<layout>
    <data>
        <variable name="viewModel" type="com.example.app.WeatherViewModel" />
    </data>

    <LinearLayout ...>
        <TextView
            android:text="@string/temperature_format(viewModel.temperature)" />
        <TextView android:text="@{viewModel.cityName}" />
        <ImageView
            android:src="@{viewModel.weatherIcon}"
            contentDescription="@{viewModel.cityName}" />
    </LinearLayout>
</layout>

Data Binding supports StateFlow starting from lifecycle 2.5.0 via Flow.asLiveData() or direct conversion. When using StateFlow in Data Binding, make sure the lifecycle is set through binding.lifecycleOwner. Without setting LifecycleOwner, LiveData/StateFlow will not update the UI because Binding does not know when the subscriber is active.

Code Examples: Data Binding in Kotlin and XML

Example 1: Profile Screen with Data Binding

A full profile screen with avatar, name, bio, and an edit button. The ViewModel uses ObservableField for reactivity.

xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable name="profile" type="com.example.app.ProfileViewModel" />
    </data>

    <androidx.constraintlayout...>
        <ImageView
            app:imageUrl="@{profile.avatarUrl}"
            android:contentDescription="@{profile.name}" />

        <TextView
            android:text="@{profile.name}"
            android:textStyle="bold" />

        <TextView
            android:text="@{profile.bio}"
            android:visibility="@{profile.hasBio ? View.VISIBLE : View.GONE}" />

        <Button
            android:onClick="@{() -> profile.onEdit()}"
            android:text="@string/edit" />
    </androidx.constraintlayout...>
</layout>
kotlin
class ProfileViewModel : ViewModel() {
    val name = ObservableField("Anna Petrova")
    val bio = ObservableField("Android developer, 5 years experience")
    val avatarUrl = ObservableField("https://example.com/avatar.jpg")
    val hasBio = ObservableBoolean(true)

    fun onEdit() {
        // Profile editing logic
    }
}

// In Fragment:
val binding = FragmentProfileBinding.inflate(inflater, container, false)
binding.profile = profileViewModel
binding.lifecycleOwner = viewLifecycleOwner

Example 2: Login Form with Two-Way Binding

A login form with field validation and a login button. Two-way binding (@={}) synchronizes user input with the ViewModel.

xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="login" type="com.example.app.LoginViewModel" />
    </data>

    <LinearLayout ...>
        <TextInputLayout>
            <TextInputEditText
                android:text="@{=login.email}"
                android:hint="@string/email_hint" />
        </TextInputLayout>

        <TextInputLayout>
            <TextInputEditText
                android:text="@{=login.password}"
                android:inputType="textPassword" />
        </TextInputLayout>

        <Button
            android:onClick="@{() -> login.onLogin()}"
            android:enabled="@{login.isValid}"
            android:text="@string/login" />

        <ProgressBar
            android:visibility="@{login.isLoading ? View.VISIBLE : View.GONE}" />
    </LinearLayout>
</layout>

In XML, expressions are used: @{login.isValid} for button state (enabled/disabled), @{login.isLoading ? View.VISIBLE : View.GONE} for the loading indicator, @{=login.email} for two-way synchronization. All validation logic lives in the ViewModel; the View only displays state. According to Google (Android Guide, 2025), this approach reduces the number of UI logic bugs by 50–60%.

Frequently Asked Questions

Can Data Binding be used with Compose?

No, Jetpack Compose is a standalone UI system with its own reactivity mechanism (Composable functions + State). Data Binding is designed exclusively for XML layouts and is incompatible with Compose. When migrating from XML to Compose, Data Binding is not used — instead, mutableStateOf(), collectAsState(), and remember are applied. Data Binding remains relevant only for projects that retain XML layouts.

Is Data Binding slower than findViewById?

At the first binding stage, Data Binding performs View lookup by ID (like findViewById). The difference is imperceptible to the user: a typical screen with 20–30 Views binds in 1–3 ms. The main overhead of Data Binding is at compile time (expression processing). At runtime, there is no difference between Data Binding and findViewById for most screens. For RecyclerView with thousands of items, ViewBinding may be faster due to less generated code.

How to debug a Data Binding error?

Data Binding compiles expressions into code at build time — errors appear in Build Output as Compilation errors with the XML line indicated. Typical errors: incorrect variable type, null-safety issues (use ?? for default values), missing class imports. Enable buildFeatures.dataBinding = true in build.gradle (app) and verify that <layout> is the root XML tag. For debugging runtime expressions, use BindingConversion and logging in BindingAdapter.

What is BindingConversion?

@BindingConversion is an annotation for static methods that automatically convert types in Data Binding expressions. For example, converting a Color Int to ColorDrawable: @BindingConversion fun colorToDrawable(color: Int): ColorDrawable = ColorDrawable(color). After this, android:background="@{color.red}" will work automatically. BindingConversions are global — they apply to all Binding expressions in the project.

Should I disable Data Binding for release builds?

No, Data Binding works in release builds just as it does in debug. ProGuard/R8 optimization may remove Binding classes if they are not used directly — add the rule: -keep class * extends android.databinding.ViewDataBinding { *; }. Starting from Android Gradle Plugin 7.0, R8 handles Data Binding correctly without additional rules. Disabling Data Binding for release does not improve performance but breaks all screens that use it.

Summary

  • Data Binding — a Jetpack library for declarative binding of XML to data via @{} and @={} expressions.
  • Generates type-safe Binding classes, eliminating findViewById() and ClassCastException.
  • ViewBinding — a lightweight alternative without expressions; choose Data Binding for reactive and complex UIs.
  • Two-way binding @={} — automatic View ↔ ViewModel synchronization for forms.
  • @BindingAdapter — custom attributes (image loading, formatting, visibility).
  • Data Binding natively supports LiveData and StateFlow via lifecycleOwner.
  • Data Binding reduces UI code by 30–50% and reduces binding bugs to zero.

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also