Introduction to Reactive Programming in Android

Reactive programming is used to build applications where the user interface automatically updates in response to real-time changes in data. For example, an application displaying live stock market updates would benefit from a reactive user interface (UI), as it can automatically refresh the data and reflect the latest stock prices in real-time without needing user intervention.
There are many advantages when using reactive programming for application development.
Automatic UI Updates
In a reactive application, the data is bound to the UI components so that when the data changes, the UI is synchronized with that change. This is especially valuable in Android, where managing UI states across different components (such as Activities, Fragments, and Views) can get complex.
Cleaner and More Maintainable Code
In reactive programming, the business logic and state management are centralized, with the state automatically bound to the UI. This eliminates the need for manual UI updates in multiple places, making the codebase more intuitive and easier to maintain. In traditional implementations, developers must manually manage UI state changes in response to data modifications. However, in reactive programming, data binding handles these updates automatically, significantly reducing the risk of inconsistencies and errors in the UI state.
Improved Performance
A reactive implementation ensures that only the components affected by data changes are updated, leading to better performance. In Android, traditional UI components like ListView can be inefficient because frequent updates often require recreating multiple views. This causes excessive consumption of memory and processing power, resulting in sluggish performance. Especially with dynamic data updates. In contrast, RecyclerView, when combined with reactive programming techniques, ensures that only the visible items update automatically in response to data changes. This minimizes unnecessary operations, improves scrolling performance, and reduces the need for manual UI state management.
Easier Testing
Testing a reactive application is simplified because the separation of data and UI allows developers to test business logic and data transformation independently. Since UI updates are managed by the reactive framework, testing the core functionality becomes more predictable and efficient.
To demonstrate reactive programming, we developed a todo application in Android using Java but before diving into the implementation, it’s important to go over the core concepts of a reactive application first.
Core Concepts
Observable
An observable represents a continuous stream of data or a source of events that can be monitored over time. Observers subscribe to an observable to receive data and respond whenever new updates are emitted. Unlike traditional methods where data must be manually checked or “polled” for updates, observers in a reactive application will automatically receive updates when data is emitted from the observable. You can think of it like a radio station. The observable is the station broadcasting a signal (the data), and observers are listeners who tune in to receive that signal.
In Android development, the LiveData class can serve as an observable data holder.
Observers
An observer is the component that listens to the observable for updates. An observer would simply “subscribe” to the observable to receive the data it emits. The purpose of the observer is to use the data it receives to accomplish some task. For example, an observer in a weather application might update a temperature display card in the UI with real-time data as the weather conditions change throughout the day.
Data Binding
Data binding is the concept of linking the UI with the underlying data in the application. This concept aims at applying UI updates when the data changes, without manual intervention. In data binding, the application’s UI is separated from the data, leading to improved code maintainability.
Streams of Data
A stream of data is simply the flow of information or a series of events that occur over time. In reactive programming, the observable emits the streams of data and the observer “subscribes” to the streams to receive the data updates in real-time.
Essential Classes in Android SDK for Building Reactive UIs
LiveData
LiveData is an observable data holder class that is lifecycle-aware. Meaning, it automatically adjusts to the lifecycle states of components like activities and fragments. One of its key advantages is that it only sends updates to components that are in an active lifecycle state (such as started or resumed). This ensures that updates are delivered only to components that can handle them, preventing unnecessary work and avoiding memory leaks. By not sending updates to components that are in an inactive or destroyed state, LiveData helps optimize performance and prevents potential memory issues.
ViewModel
A ViewModel is a class that manages and prepares UI-related data for an Android app. Its main advantage is the ability to persist this data across configuration changes, such as screen rotations. Therefore, ensuring the UI retains the necessary state. In addition, the ViewModel offers an API that streamlines state management, relieving developers from the need to manually handle UI data persistence during configuration changes.
RecyclerView
RecyclerView is a flexible view used to display large amounts of data in a list or grid. It’s more efficient than older views like ListView because it reuses items that are no longer visible, which helps save memory and improve performance. RecyclerView also lets you customize how your items are arranged, making it ideal for complex layouts and smooth scrolling in apps with lots of data.
ListAdapter
ListAdapter is a specialized adapter used with RecyclerView to efficiently manage and display a list of data. It is particularly useful for handling lists that are frequently updated, as it automatically detects changes in the data and updates the UI with minimal effort, ensuring smoother performance and reducing the need for manual notifications of item changes.
ViewHolder
A ViewHolder is an object that holds (i.e caches) references to the views within a list item layout for performance optimization. This approach prevents repeated calls to findViewById() during scrolling, ensuring smoother and more efficient view recycling.
Checkout the Todo app’s source code to see how these classes work in action.
The Todo App
Overview
The following animation shows a sample demonstration of the todo app.

The following screenshot shows the project structure in Android Studio.

Source Code
Dependencies
In build.gradle.kts under Gradle Scripts, the dependencies are as follows.
dependencies {
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.activity)
implementation(libs.constraintlayout)
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
}
build.gradle.kts is the Kotlin DSL (Domain Specific Language) version of a Gradle build script. If you are using build.gradle (Groovy DSL), then you need to adapt it accordingly.
Also under Gradle Scripts, the content of libs.versions.toml is as follows.
[versions]
agp = "8.9.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
appcompat = "1.6.1"
material = "1.10.0"
activity = "1.8.0"
constraintlayout = "2.1.4"
[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
Tip
Keep versions updated in your libs.versions.toml file. Check Maven Central Repository or AndroidX Release Notes for the latest.
Java Classes
Each subsection provides the source code of a Java class or interface. All classes and interfaces should go in the package com.example.reactiveuidemo.
Todo
package com.example.reactiveuidemo;
import android.util.Log;
public class Todo {
private final String m_description;
private boolean m_isDone;
public Todo(String description) {
m_description = description;
m_isDone = false;
}
public String getDescription() {
return m_description;
}
public boolean isDone() {
return m_isDone;
}
public void setDone(boolean isDone) {
m_isDone = isDone;
}
public boolean hasSameContentAs(Todo other) {
return other.isDone() == m_isDone && other.getDescription().equals(m_description);
}
}
DeleteTodoCallback
package com.example.reactiveuidemo;
public interface DeleteTodoCallback {
void onDelete(Todo todo);
}
TodoViewHolder
package com.example.reactiveuidemo;
import android.content.Context;
import android.graphics.Paint;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.checkbox.MaterialCheckBox;
public class TodoViewHolder extends RecyclerView.ViewHolder {
private final TextView m_todoTxt;
private final Button m_deleteBtn;
private final MaterialCheckBox m_checkbox;
private final DeleteTodoCallback m_deleteTodoCallback;
private final Context m_context;
public TodoViewHolder(@NonNull View itemView, DeleteTodoCallback deleteTodoCallback) {
super(itemView);
m_context = itemView.getContext();
m_deleteTodoCallback = deleteTodoCallback;
m_todoTxt = itemView.findViewById(R.id.todoTxt);
m_deleteBtn = itemView.findViewById(R.id.deleteTodoBtn);
m_checkbox = itemView.findViewById(R.id.checkbox);
}
/**
* Sets the behavior and state of each UI component of the item view.
*/
public void bind(Todo todo) {
m_todoTxt.setText(todo.getDescription());
// Use the DeleteTodoCallback to define delete button's behavior.
m_deleteBtn.setOnClickListener(view -> m_deleteTodoCallback.onDelete(todo));
m_checkbox.setChecked(todo.isDone());
// Define the behavior of the checkbox.
m_checkbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
todo.setDone(isChecked);
if (isChecked) {
// Strike the todo when the checkbox is checked.
strikeTodo();
} else {
// Removing the strikethrough line on the todo when the checkbox is unchecked.
restoreTodo();
}
});
}
/**
* Removes the strikethrough line on the todo and restores its original text color.
*/
private void restoreTodo() {
m_todoTxt.setPaintFlags(m_todoTxt.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
m_todoTxt.setTextColor(ContextCompat.getColor(m_context, R.color.on_surface_emphasis_high));
}
/**
* Applies a strikethrough to the todo and changes its text color to serve as a visual cue for its
* completed status.
*/
private void strikeTodo() {
m_todoTxt.setPaintFlags(m_todoTxt.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
m_todoTxt.setTextColor(ContextCompat.getColor(m_context, R.color.on_surface_disabled));
}
/**
* Helper method for creating the TodoViewHolder.
*
* @param parent parent ViewGroup of the TodoListAdapter.
* @param deleteTodoCallback callback that handles todo deletion.
* @return TodoViewHolder instance.
*/
static TodoViewHolder create(ViewGroup parent, DeleteTodoCallback deleteTodoCallback) {
// Create the view to be bound to each item of the recycler view.
View view = LayoutInflater.from(parent.getContext()).inflate(
R.layout.todo_recyclerview_item,
parent,
false);
return new TodoViewHolder(view, deleteTodoCallback);
}
}
TodoViewModel
package com.example.reactiveuidemo;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Class that manages the data.
*/
public class TodoViewModel extends ViewModel {
// Declaring a mutable live data so new lists can be set.
private final MutableLiveData<List<Todo>> _todos = new MutableLiveData<>(new ArrayList<>());
// The observable todo list, which is public so it's accessible to the observers.
public final LiveData<List<Todo>> m_todos = _todos;
/**
* Adds a new todo and updates the live data (i.e observable).
*
* @param description of the todo task in natural language.
*/
public void addTodo(String description) {
if (_todos.getValue() == null) {
_todos.setValue(new ArrayList<>());
}
List<Todo> todos = _todos.getValue();
todos.add(new Todo(description));
// Set the list again to trigger the observer.
_todos.setValue(todos);
}
/**
* Deletes a todo and updates the live data.
* @param todo the Todo instance.
*/
public void delete(Todo todo) {
List<Todo> todos = _todos.getValue();
Objects.requireNonNull(todos).remove(todo);
_todos.setValue(todos);
}
}
TodoListAdapter
package com.example.reactiveuidemo;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
public class TodoListAdapter extends ListAdapter<Todo, TodoViewHolder> {
private final DeleteTodoCallback m_deleteTodoCallback;
protected TodoListAdapter(@NonNull DiffUtil.ItemCallback<Todo> diffCallback,
DeleteTodoCallback deleteTodoCallback) {
super(diffCallback);
m_deleteTodoCallback = deleteTodoCallback;
}
@NonNull
@Override
public TodoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return TodoViewHolder.create(parent, m_deleteTodoCallback);
}
@Override
public void onBindViewHolder(@NonNull TodoViewHolder holder, int position) {
Todo todo = getItem(position);
// Binding a todo object to the UI.
holder.bind(todo);
}
/**
* Class used for calculating the difference between two Todo instances.
* This is used by the adapter to handle data changes efficiently.
*/
static class TodoDiff extends DiffUtil.ItemCallback<Todo> {
@Override
public boolean areItemsTheSame(@NonNull Todo oldItem, @NonNull Todo newItem) {
return oldItem == newItem;
}
@Override
public boolean areContentsTheSame(@NonNull Todo oldItem, @NonNull Todo newItem) {
return oldItem.hasSameContentAs(newItem);
}
}
}
TodoListObserver
package com.example.reactiveuidemo;
import java.util.ArrayList;
import java.util.List;
import androidx.lifecycle.Observer;
/**
This observer sends a new list of todos to the TodoListAdapter on receiving updates from the
observable (i.e the LiveData that holds the list of todo objects).
*/
public class TodoListObserver implements Observer<List<Todo>> {
private final TodoListAdapter m_adapter;
public TodoListObserver(TodoListAdapter adapter) {
super();
m_adapter = adapter;
}
/**
* Submits a new list of todos to the recycler view adapter.
*
* @param todos list of Todo objects.
*/
@Override
public void onChanged(List<Todo> todos) {
// This call informs the RecyclerView of new data.
// Whether a UI update occurs depends on the result of the data diff.
m_adapter.submitList(new ArrayList<>(todos));
}
}
MainActivity
package com.example.reactiveuidemo;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class MainActivity extends AppCompatActivity implements DeleteTodoCallback {
private TodoViewModel m_viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(
findViewById(R.id.main),
(v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
TodoListAdapter adapter = new TodoListAdapter(new TodoListAdapter.TodoDiff(), this);
m_viewModel = new ViewModelProvider(this).get(TodoViewModel.class);
// Assigning an observer to the todo list observable.
m_viewModel.m_todos.observe(this, new TodoListObserver(adapter));
RecyclerView todosRecyclerView = findViewById(R.id.todosRecyclerView);
todosRecyclerView.setLayoutManager(new LinearLayoutManager(this));
todosRecyclerView.setAdapter(adapter);
EditText todoEditText = findViewById(R.id.newTodoTxt);
Button addTodoBtn = findViewById(R.id.addTodoButton);
addTodoBtn.setOnClickListener(view -> {
String newTodo = todoEditText.getText().toString();
if (!newTodo.isEmpty()) {
m_viewModel.addTodo(newTodo);
}
todoEditText.setText("");
});
}
@Override
public void onDelete(Todo todo) {
m_viewModel.delete(todo);
}
}
Layouts
The following subsections show the layout files under res/layout.
main_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/newTodoTxtLayout"
style="?attr/textInputFilledStyle"
android:hint="@string/todo_label"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/addTodoButton"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/newTodoTxt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/addTodoButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="@string/add_label"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/newTodoTxtLayout"
app:layout_constraintBottom_toTopOf="@id/divider"
/>
<com.google.android.material.divider.MaterialDivider
android:id="@+id/divider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/newTodoTxtLayout" />
<TextView
android:id="@+id/todosTitle"
style="@style/TextAppearance.Material3.TitleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:textStyle="bold"
android:text="@string/todo_list_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/todosRecyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:nestedScrollingEnabled="false"
android:contentDescription="@string/todos_desc"
tools:listitem="@layout/todo_recyclerview_item"
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/todosTitle"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
todo_recyclerview_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/todoTxt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:paddingStart="0dp"
android:paddingEnd="14dp"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/on_surface_emphasis_high"
app:layout_constraintStart_toEndOf="@id/checkbox"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/deleteTodoBtn"
style="?attr/materialIconButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/delete_btn_desc"
app:icon="@drawable/baseline_close_24"
app:iconSize="20dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/todoTxt" />
</androidx.constraintlayout.widget.ConstraintLayout>
Values
The following subsections show the values under res/layout.
strings.xml
<resources>
<string name="app_name">Reactive UI Demo</string>
<string name="todo_label">Todo</string>
<string name="add_label">Add</string>
<string name="todo_list_title">Todo List</string>
<string name="todos_desc">List of tasks to do</string>
<string name="delete_btn_desc">Deletes the todo</string>
</resources>
colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="on_surface_emphasis_high">@color/material_on_surface_emphasis_high_type</color>
<color name="on_surface_disabled">@color/material_on_surface_disabled</color>
</resources>
Conclusion
Reactive programming offers a powerful paradigm for handling asynchronous events and managing complex data flows in Android applications. By embracing this approach, developers can write cleaner and more maintainable code that responds efficiently to user input and system events.
We explored how LiveData enables reactive programming in Android by allowing your app to efficiently observe and respond to data changes. LiveData provides a simple, lifecycle-aware approach to building reactive UIs, making it a great starting point for developers new to this paradigm. While LiveData covers many common use cases, it’s just one tool in the broader reactive programming landscape. As your apps grow more complex, you may find it valuable to explore more advanced frameworks like RxJava, which offers greater flexibility and control over asynchronous data streams.
Reactive programming can seem like a shift in mindset at first, but it pays off in cleaner architecture and more responsive apps. Start small with LiveData, as you’ve seen in our demo, and don’t hesitate to branch out into more powerful tools as your confidence grows.
We Need Your Help!
Please check out our apps and games, we also have a few Unity icon packs in the Unity Asset Store if you need them for your game or app. Your support is very much appreciated!
You Might Also Like
- Wooden Icons Pack Available in the Unity Asset Store.
- Glossy Icons Pack Available in the Unity Asset Store.
- Flat Icons Pack Available in the Unity Asset Store.
- How to Implement In-App Purchases in an Android App.
- Note Chain Available Now!
- Tips for Developing a Game as a Solo Developer.
- How to Make a 2D Rain Effect in the Unity Engine.
- Tips for Debugging Unity Games.
- How to Improve the Performance of Your Unity Game.
- Thirsty Plant Available Now!
- How to Implement In-App Purchases with Unity IAP.
- How to Create a Dripping Liquid Effect in the Unity Engine.
- Introduction to Reactive Programming in Android