Two-Way Binding: What Is Bidirectional Data Binding in Android and iOS

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

Learn what Two-Way Binding is — bidirectional data binding that automatically synchronizes the model and view in mobile applications. Unlike manual UI updates via findViewById, the binding mechanism updates both the model when user input changes and the view when data changes. According to Google I/O 2024, binding reduces boilerplate UI code by 30–50% in Android and iOS projects. The approach is used in frameworks — from Jetpack Compose and SwiftUI to Flutter and React Native.

Key Takeaways

  • Two-Way Binding — a mechanism that automatically synchronizes data between the model (ViewModel) and the view in both directions.
  • In Android it is implemented via @BindingAdapter and @= in DataBinding, in iOS — via @Binding in SwiftUI.
  • According to Google, DataBinding reduces UI code volume by 30–50% compared to manual binding via findViewById.
  • The main danger is infinite update loops caused by incorrectly configured change listeners.
  • In modern development, unidirectional data flow (UDF) with explicit events is preferred, while Two-Way Binding is used selectively for input forms.

What Is Two-Way Binding?

Two-Way Binding (bidirectional data binding) — an architectural mechanism where changes in the data model are automatically reflected in the user interface, and changes in the UI immediately update the model. Unlike one-way binding, where data flows only from the model to the view, bidirectional binding creates a closed synchronization loop without manual coding of each update.

According to the Android Developers Blog (2023), the DataBinding library, introduced in 2015, is used in 42% of commercial Android applications. The mechanism is especially in demand in input forms — text fields, switches, sliders and checkboxes — where user input must be instantly reflected in the model and programmatic changes in the UI. In all these scenarios, the developer writes one binding instead of a pair of "listener + setter".

At IT Sectr, we have applied bidirectional binding in projects since 2017 and recommend using it consciously: for simple input fields, but not for complex states with dependencies.

How Does Bidirectional Binding Work?

The Two-Way Binding mechanism is built on three key elements: observable field, change listener and reverse synchronization mechanism. When a user types text in an EditText field, the system intercepts the TextWatcher event, writes the new value to the bound variable, and notifies the UI to redraw if the variable changed from code.

Under the hood, the Android DataBinding library generates a Binding class at compile time that contains all the binding logic. For each View with the @={variable} attribute, a pair of setter + getter with invalidation is created. In SwiftUI, the @Binding propertyWrapper performs similar work, synchronizing the value through the Combine mechanism. SwiftUI tracks changes via @Published properties and automatically redraws the View on any change to the bound variable.

According to WWDC Session 10033 (2023), the @Binding mechanism in SwiftUI processes up to 60 frames per second when synchronizing input fields, making it suitable for interactive forms without lag. In both frameworks, Two-Way Binding is syntactic sugar over the Observer pattern, automating subscription and notification.

Two-Way Binding in Android: DataBinding and Jetpack Compose

In Android, bidirectional binding is available in two variants: classic XML DataBinding via the @={} attribute and Jetpack Compose via bidirectional state references. Both approaches solve the same problem — synchronizing UI and model — but differ in syntax and scope.

DataBinding with @BindingAdapter and @=

In XML markup, bidirectional binding is denoted by the @={variable.property} syntax — the equals sign inside curly braces distinguishes it from one-way @{variable}. For custom Views, the @BindingAdapter annotation with an inverse attribute is required.

XML
<layout>
    <data>
        <variable name="viewModel" type="com.example.LoginViewModel" />
    </data>
    <EditText
        android:text="@{viewModel.email}" />
    <CheckBox
        android:checked="@{viewModel.agreeToTerms}" />
</layout>

The example shows a simple form with email and checkbox — both fields use bidirectional binding, which eliminates writing TextWatcher and OnCheckedChangeListener in Activity code. When the user changes the text, the viewModel.email field updates automatically.

Kotlin
@BindingAdapter("app:rating")
fun RatingBar.setRating(rating: Float) {
    if (rating != this.rating) {
        this.rating = rating
    }
}

@InverseBindingAdapter("app:rating")
fun RatingBar.getRating(): Float = this.rating

@BindingAdapter("app:ratingAttrChanged")
fun RatingBar.setListeners(
    listener: InverseBindingListener?
) {
    this.onRatingBarChangeListener =
        RatingBar.OnRatingBarChangeListener { _, _, _ -> listener?.onChange() }
}

A custom BindingAdapter for RatingBar uses a pair of annotations — @BindingAdapter and @InverseBindingAdapter — so the DataBinding library knows how to read the value from the View (reverse feedback) and how to write to the View (direct binding). The third adapter with the AttrChanged suffix notifies the system of user-initiated value changes.

Two-Way Binding in Jetpack Compose

Jetpack Compose does not support the @={} syntax but provides a similar mechanism via mutableStateOf and explicit setter function passing. Bidirectional binding in Compose is built on passing State and a callback function (value, onValueChange) to child components.

Kotlin
@Composable
fun LoginScreen() {
    var email by remember { mutableStateOf("") }

    OutlinedTextField(
        value = email,
        onValueChange = { email = it },
        label = { Text("Email") }
    )
}

@Composable
fun CustomRatingBar(
    rating: Float,
    onRatingChange: (Float) -> Unit
) {
    Slider(
        value = rating,
        onValueChange = onRatingChange,
        valueRange = 0f..5f
    )
}

In Compose, bidirectional communication is emulated through a state + callback pair — the parent passes the current value and an update function, the child component invokes the callback on user interaction. This approach explicitly shows the data flow direction, simplifying debugging compared to implicit DataBinding synchronization.

Two-Way Binding in iOS: @Binding in SwiftUI

In SwiftUI, bidirectional binding is implemented via the @Binding propertyWrapper, which creates a read-write reference to a data source owned by the parent View. @Binding does not store the value itself — it reads and writes through the parent's @State or @StateObject.

Swift
struct LoginView: View {
    @State private var email = ""
    @State private var agreeToTerms = false

    var body: some View {
        Form {
            TextField("Email", text: $email)
            Toggle("I agree to the terms", isOn: $agreeToTerms)
            ChildRatingView(rating: $rating)
        }
    }
}

struct ChildRatingView: View {
    @Binding var rating: Double

    var body: some View {
        Slider(value: $rating, in: 0...5)
    }
}

The $ symbol before a variable name creates a Binding reference: $email has type Binding, not String. SwiftUI automatically links text changes in TextField to updating the email property through the Combine mechanism. The parent View passes a Binding to its @State to the child component, allowing state modification from any hierarchy level without delegates or callbacks.

According to Apple WWDC 2023, SwiftUI uses a diffing algorithm to minimize redraws: if the @Binding value changes but the View does not depend on that value, no redraw occurs. This provides performance comparable to UIKit (up to 120 FPS on ProMotion displays).

Two-Way Binding vs UDF: When to Choose What

The choice between bidirectional binding and unidirectional data flow (UDF) is one of the key architectural decisions in mobile development. Two-Way Binding is optimal for local form states where each user step must be immediately reflected in the model without additional code. UDF is preferable for global application state where change predictability matters more than development speed.

CriterionTwo-Way BindingUDF
Code volume in form1 line (@={} attribute)5–7 lines (State, Intent, Reducer)
Data flow debuggingDifficult (who changed it — UI or code?)Easy (all changes through Intent)
PerformanceHigh (native synchronization)Medium (Reducer + Redux layer)
ScalabilityDecreases on complex forms with validationIncreases with number of screens
State predictabilityLow (side effects from loops)High (reducer is the single source of truth)

Recommendation: use Two-Way Binding for simple input fields (text, checkboxes, switches) in forms with 3–5 fields without complex validation. For screens with global state, network requests and dependent fields, use UDF with unidirectional flow and explicit event handling. At IT Sectr, we combine both approaches: Two-Way Binding inside forms, UDF for navigation and business logic.

Common Mistakes in Bidirectional Binding

Infinite update loop — the most common problem when using Two-Way Binding. The loop occurs when a model change triggers a UI update, which in turn changes the model again. In DataBinding, this happens if the getter in @InverseBindingAdapter returns a new value immediately after a setter call. The solution is to check whether the value changed before writing back (guard condition).

The second common mistake is binding computed fields. If a field depends on another field (e.g., total cost = price × quantity), bidirectional binding can lead to an inconsistent state. For example, the user changes the quantity, triggering a cost recalculation, which changes the quantity again. For computed fields, use one-way binding with Flow or Combine.

The third mistake is binding Observable fields without LifecycleOwner. In Android DataBinding, a LifecycleOwner must be passed to the binding, otherwise observers will not be cleaned up when the Activity is destroyed, leading to memory leaks. Always pass viewLifecycleOwner in fragments and this in Activity.

According to Google Issue Tracker (2024), about 15% of DataBinding bug reports are related to cyclic updates. For diagnostics, use Android Studio Layout Inspector — it shows current values of all bindings on the screen, simplifying the search for the infinite loop source.

Frequently Asked Questions

How is bidirectional binding different from one-way binding?

One-way binding transfers data only from the model to the view — when the model changes, the UI updates, but user input does not directly change the model. Two-Way Binding synchronizes data in both directions: a change in the UI automatically updates the model, and vice versa. In DataBinding syntax, the difference is denoted by @{} (One-Way) and @={} (Two-Way).

When should Two-Way Binding not be used?

Do not use bidirectional binding for complex forms with dependent fields, computed values or custom validation — in these scenarios, the data flow becomes unpredictable. Also avoid it in lists like RecyclerView with a large number of items where each item has binding: performance degrades due to many observers. UDF with unidirectional flow and Intent-based event handling scales better.

Does Jetpack Compose support bidirectional binding?

Jetpack Compose does not have built-in @={} syntax, but bidirectional synchronization is implemented via a State + callback (onValueChange) pair. The parent passes the current value (State) and an update function, the child component invokes the callback on change. This is explicit rather than implicit binding — the data flow remains visible and traceable.

How to debug an infinite loop in DataBinding?

To debug loops in DataBinding, use Android Studio Layout Inspector — it shows current values of all bound variables on the screen. Add logging in @InverseBindingAdapter and check whether the getter returns a value different from the one just written. The standard solution is a guard condition: if (newValue != currentValue) before writing back.

Is there Two-Way Binding in Flutter?

Flutter does not have built-in bidirectional binding, but it can be emulated through a combination of TextEditingController and the onChanged callback. For StatefulWidget, the developer manually subscribes to controller changes and updates the model. In Provider and Riverpod, bidirectional synchronization is built through Selector, which rebuilds the widget when the model changes and invokes a callback on user input.

Summary

  • Two-Way Binding — an automatic bidirectional synchronization mechanism between the model and view, eliminating manual writing of listeners and setters.
  • In Android, it is implemented via DataBinding with @={} syntax and @BindingAdapter/@InverseBindingAdapter annotations.
  • In iOS, SwiftUI provides the @Binding propertyWrapper, creating a read-write reference to the parent's @State.
  • DataBinding reduces UI code volume by 30–50%, but complicates debugging when infinite loops appear.
  • For forms with 3–5 fields, Two-Way Binding is effective; for global state and complex validation, choose UDF.
  • In Jetpack Compose, bidirectional communication is emulated through State + onValueChange callback, preserving explicit data flow.
  • Main risks are cyclic updates, binding computed fields, and memory leaks when LifecycleOwner is missing.

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