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>