Integrating Android Architecture Components: ViewModel, LiveData, and Data Binding

Managing Lifecycle-Aware Application State

Core ViewModel Implementation

The ViewModel class acts as a lifecycle-conscious container for UI-related data. It survives configuration changes such as screen rotations without triggering recreation.

package com.tech.samples.ui;

import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;

public class DashboardActivity extends AppCompatActivity {

    private TextView statusLabel;
    private AppStateViewModel stateHolder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_dashboard);
        
        statusLabel = findViewById(R.id.statusLabel);
        // Retrieve or create the ViewModel scoped to this activity
        stateHolder = new ViewModelProvider(this).get(AppStateViewModel.class);
        
        updateUI();
    }

    public void incrementCounter(android.view.View view) {
        stateHolder.updateValue(stateHolder.getValue() + 1);
        updateUI();
    }

    private void updateUI() {
        statusLabel.setText(String.valueOf(stateHolder.getValue()));
    }
}
package com.tech.samples.viewmodel;

import androidx.lifecycle.ViewModel;

public class AppStateViewModel extends ViewModel {
    private int currentValue = 0;

    public int getValue() {
        return currentValue;
    }

    public void updateValue(int newValue) {
        this.currentValue = newValue;
    }
}

Reactive Updates with LiveData

Wrapping state in LiveData enables automatic UI notifications when underlying data changes. This decouples observers from direct polling mechanisms.

package com.tech.samples.viewmodel;

import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;

public class TimerDataModel extends ViewModel {
    private final MutableLiveData<Integer> elapsedSeconds = new MutableLiveData<>();

    public MutableLiveData<Integer> getElapsedSeconds() {
        if (elapsedSeconds.getValue() == null) {
            elapsedSeconds.setValue(0);
        }
        return elapsedSeconds;
    }
}
package com.tech.samples.ui;

import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.TextView;
import java.util.Timer;
import java.util.TimerTask;

public class MonitoringActivity extends AppCompatActivity {

    private TextView timerDisplay;
    private TimerDataModel model;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_monitoring);
        
        timerDisplay = findViewById(R.id.timerDisplay);
        model = new ViewModelProvider(this).get(TimerDataModel.class);
        
        // Observe live data updates
        model.getElapsedSeconds().observe(this, new Observer<Integer>() {
            @Override
            public void onChanged(Integer seconds) {
                if (seconds != null) {
                    timerDisplay.setText(String.valueOf(seconds));
                }
            }
        });

        startBackgroundTimer();
    }

    private void startBackgroundTimer() {
        Handler mainHandler = new Handler(Looper.getMainLooper());
        new Timer().scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                int nextValue = model.getElapsedSeconds().getValue() + 1;
                // Use postValue for background threads to avoid threading violations
                model.getElapsedSeconds().postValue(nextValue);
            }
        }, 1000, 1000);
    }
}

Facilitating Cross-Component Communication

Shared ViewModel Across Fragmetns

When multiple fragments require access to the same dataset, attaching a ViewModel to the parenet activity ensures they share an identical instance throughout their lifecycle.

// Fragment A: Controls value
public class ControlFragment extends Fragment {
    private SharedDataModel sharedModel;

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.fragment_control, container, false);
        SeekBar slider = root.findViewById(R.id.progressSlider);
        
        // Scope to parent Activity to share instance
        sharedModel = new ViewModelProvider(requireActivity()).get(SharedDataModel.class);

        sharedModel.getProgressLevel().observe(getViewLifecycleOwner(), progress -> {
            slider.setProgress(progress);
        });

        slider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                sharedModel.setProgressLevel(progress);
            }
            @Override public void onStartTrackingTouch(SeekBar seekBar) {}
            @Override public void onStopTrackingTouch(SeekBar seekBar) {}
        });

        return root;
    }
}
// Fragment B: Displays value
public class DisplayFragment extends Fragment {
    private SharedDataModel sharedModel;

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.fragment_display, container, false);
        SeekBar slider = root.findViewById(R.id.targetSlider);

        sharedModel = new ViewModelProvider(requireActivity()).get(SharedDataModel.class);

        sharedModel.getProgressLevel().observe(getViewLifecycleOwner(), progress -> {
            slider.setProgress(progress);
        });

        return root;
    }
}
package com.tech.samples.viewmodel;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;

public class SharedDataModel extends ViewModel {
    private final MutableLiveData<Integer> progressLevel = new MutableLiveData<>(50);

    public MutableLiveData<Integer> getProgressLevel() { return progressLevel; }
    public void setProgressLevel(int val) { progressLevel.setValue(val); }
}

Streamlining UI Development with Data Binding

Project Configuration

Enable declarative binding through Gradle feature flags:

android {
    buildFeatures { dataBinding = true }
}
dependencies {
    implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0"))
}

Declarative Layout Associations

Data binding eliminates findViewById by generating binding classes that map directly to layout XML structures.

Layout (activity_profile.xml):

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable name="profileInfo" type="com.tech.samples.model.ProfileCard" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent" android:layout_height="match_parent">
        <TextView
            android:text="@{profileInfo.displayName}" />
        <TextView
            android:text="@{profileInfo.ratingLevel}" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Activity Implementation:

package com.tech.samples.ui;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import com.tech.samples.databinding.ActivityProfileBinding;
import com.tech.samples.model.ProfileCard;

public class ProfileViewerActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityProfileBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_profile);
        ProfileCard card = new ProfileCard("Alex Mercer", "Gold Tier");
        binding.setProfileInfo(card);
    }
}

Leveraging Expression Language and Imports

Utility functions can be exposed in XML using <import> tags. Event handlers should be passed via binding variables rather than hardcoded XML attributes.

package com.tech.samples.utils;
public class FormatUtils {
    public static String formatRating(int stars) {
        switch(stars) {
            case 1: return "Bronze";
            case 2: return "Silver";
            case 3: return "Gold";
            default: return "Unranked";
        }
    }
}
package com.tech.samples.handlers;
import android.content.Context;
import android.view.View;
import android.widget.Toast;
public class UiActionHandler {
    private Context ctx;
    public UiActionHandler(Context context) { this.ctx = context; }
    public void submitAction(View view) {
        Toast.makeText(ctx, "Action Confirmed", Toast.LENGTH_SHORT).show();
    }
}

Updated Layout:

<data>
    <variable name="item" type="com.tech.samples.model.ProfileCard" />
    <variable name="handler" type="com.tech.samples.handlers.UiActionHandler" />
    <import type="com.tech.samples.utils.FormatUtils" />
</data>
<TextView android:text="@{FormatUtils.formatRating(item.score)}" />
<Button android:onClick="@{handler.submitAction}" android:text="Submit" />

Building Reusable Custom Attributes

Custom @BindingAdapter methods allow seamless integration of third-party image loading libraries directly within XML.

package com.tech.samples.adapters;
import android.graphics.Color;
import android.text.TextUtils;
import android.widget.ImageView;
import androidx.databinding.BindingAdapter;
import com.squareup.picasso.Picasso;

public class ImageBindingExtensions {
    @BindingAdapter(value = {"imageUrl", "placeholderId"}, requireAll = false)
    public static void loadImage(ImageView imageView, String url, int placeholderId) {
        if (!TextUtils.isEmpty(url)) {
            Picasso.get()
                .load(url)
                .placeholder(placeholderId)
                .into(imageView);
        } else {
            imageView.setImageResource(placeholderId);
        }
    }
}

Usage in XML:

<ImageView
    app:imageUrl="@{mediaItem.thumbnail}"
    app:placeholderId="@{R.drawable.default_avatar}" />

Implementing Two-Way Data Synchronization

Two-way binding synchronizes data between views and models bidirectionally using @={} syntax. Two primary patterns exist:

Pattern 1: BaseObservable

Requires explicit change notifications via notifyPropertyChanged().

public class ObservableUserProfile extends BaseObservable {
    private String username;

    public ObservableUserProfile(String initialName) {
        this.username = initialName;
    }

    @Bindable
    public String getUsername() { return username; }

    public void setUsername(String name) {
        if (!name.equals(this.username)) {
            this.username = name;
            notifyPropertyChanged(BR.username);
        }
    }
}

Pattern 2: ObservableField

Handles notifications automatically by wrapping values.

public class LightweightProfile {
    public final ObservableField<String> displayName = new ObservableField<>("Guest");
}

Layout Syntax:

<EditText
    android:text="@={profileModel.username}" />
<!-- For ObservableField -->
<EditText
    android:text="@={profileModel.displayName.get()}" />

Optimizing List Rendering Performance

Integrate Data Binding efficiently into RecyclerView adapters by inflating bindings directly during view creation.

package com.tech.samples.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.tech.samples.databinding.ItemCardBinding;
import com.tech.samples.model.ItemEntry;
import java.util.List;

public class ItemGridAdapter extends RecyclerView.Adapter<ItemGridAdapter.ViewHolder> {
    private final List<ItemEntry> dataSource;

    public ItemGridAdapter(List<ItemEntry> items) { this.dataSource = items; }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        ItemCardBinding binding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.getContext()),
            R.layout.item_card,
            parent,
            false
        );
        return new ViewHolder(binding);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        ItemEntry entry = dataSource.get(position);
        holder.binding.setItemData(entry);
        holder.binding.executePendingBindings();
    }

    @Override
    public int getItemCount() { return dataSource.size(); }

    static class ViewHolder extends RecyclerView.ViewHolder {
        ItemCardBinding binding;
        ViewHolder(ItemCardBinding b) { super(b.getRoot()); this.binding = b; }
    }
}

Item Layout (item_card.xml):

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable name="itemData" type="com.tech.samples.model.ItemEntry" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent" android:layout_height="wrap_content">
        <ImageView app:itemImage="@{itemData.imageUrl}" />
        <TextView android:text="@{itemData.title}" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Tags: Android viewmodel livedata DataBinding ArchitectureComponents

Posted on Thu, 24 Sep 2026 16:55:30 +0000 by motofzr1000