
In the world of Android UI development, DiffUtil
is a go-to tool for optimizing list updates in RecyclerView
. Enter DiffUtils, a utility class that calculates the minimal changes required to transform one list into another and updates only the parts of the UI that changed, saving performance and reducing unnecessary redraws. This powerful tool relies on Myers’ Algorithm, an efficient method for finding the shortest edit script between two sequences.
But in Jetpack Compose, DiffUtil is conspicuously absent. Why? 🤔
In this blog, I will break down how DiffUtils works, its connection to Myers’ algorithm, why it’s an essential part of modern Android development and explores why DiffUtil
is unnecessary in Compose, how Compose optimizes UI updates, and what you should use instead.
Let’s dive in! 🏊
- 🧑🎓 What is DiffUtils?
- 💡 Why does Jetpack Compose not need
DiffUtil?
- 🌟 Interview Question
- ✅ Conclusion
- 🌍 References
🧑🎓 What is DiffUtils?
DiffUtils is a utility in Android that compares two lists and generates a sequence of update operations, such as:
- Insertions: Adding new items.
- Deletions: Removing obsolete items.
- Moves: Reordering existing items.
These operations can then be applied to update a list efficiently, minimizing unnecessary redraws or recalculations. This is particularly useful in components like RecyclerView, where performance matters.
How DiffUtils Works
DiffUtil uses Eugene W. Myers’s difference algorithm to calculate the minimal number of updates to convert one list into another. Myers’s algorithm does not handle items that are moved so DiffUtil runs a second pass on the result to detect items that were moved. — https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil
DiffUtils in Android is based on Myers’ algorithm for comparing lists and finding the differences between them. The goal of both Myers’ algorithm and DiffUtils is the same: to determine the minimal number of changes required to transform one sequence (list) into another, which includes insertions, deletions, and moves. At its core, DiffUtils computes the differences between two lists by identifying the:
- Longest Common Subsequence (LCS): Elements that remain unchanged between the old and new lists.
- Edit Operations: Insertions, deletions, and moves required to convert the old list into the new one.
Key Insight:
- DiffUtils uses the concepts of LCS to minimize changes (insertions and deletions), and it also optimizes for moves — a feature specific to list-based comparisons where elements are not merely deleted and inserted but are repositioned.
Myers’ Algorithm: The Foundation of DiffUtils
Myers’ Algorithm, introduced in 1986, is designed to compute the shortest edit script (SES) between two sequences. It finds the minimal number of operations needed to transform one sequence into another. These operations include:
- Insertions
- Deletions
- Matches (common elements in order)
Key Concepts of Myers’ Algorithm
Myers’ algorithm is designed to find the smallest number of insertions, deletions, and moves needed to transform one sequence (say, an old list) into another sequence (the new list). The algorithm operates on the concept of edit distance and specifically calculates a series of operations to transform one sequence into another.
Myers’ algorithm is particularly efficient in calculating the shortest sequence of edit operations, minimizing the total number of changes required. The core idea is to find the longest common subsequence (LCS) between two sequences and then determine the minimal operations to convert the old sequence into the new sequence.
public int longestCommonSubsequence(String text1, String text2) { if (text1.isEmpty() || text2.isEmpty()) { return 0; } int m = text1.length(); int n = text2.length(); // Create a 2D array to store the lengths of LCS int[][] dp = new int[m + 1][n + 1]; for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (text1.charAt(i - 1) == text2.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); } } } // The length of the longest common subsequence return dp[m][n]; }
Longest Common Subsequence (LCS): Myers’ algorithm first identifies the LCS between the two lists. The LCS represents the elements that do not need modification.
Edit Graph:
- The algorithm visualizes the transformation as a graph, where each path represents a series of operations (insertions, deletions, or matches).
- The shortest path through this graph corresponds to the shortest edit script (SES).
Optimization:
- Myers’ algorithm uses dynamic programming to reduce computational overhead, achieving an efficient O(ND) time complexity, where N and D are the lengths of the sequences and the distance between them.
An Example of DiffUtils in Action
Let’s consider two lists:
// Old list ["a", "b", "c", "d"] // New List ["a", "d", "c", "b"]:
- Identify LCS: The LCS here is
["a"]
. - Compute Edit Script:
- Delete
"b"
(old list). - Move
"d"
before"c"
. - Insert
"b"
after"c"
.
3. Apply Changes: The old list is transformed into the new list using these minimal operations.
The Role of DiffUtil in RecyclerView
In RecyclerView, every update involves calculating which items changed, which were added, and which were removed. Naively updating the entire list can lead to performance issues like jank or unresponsiveness. DiffUtils solves this by:
- Minimizing Changes: Only the necessary updates are performed.
- Optimizing Performance: Smooth animations and efficient list updates are achieved.
- Reducing Redraws: Only affected items are re-rendered, improving overall UI responsiveness.
// Step 1: Create a DiffUtil.Callback public class MyDiffCallback extends DiffUtil.Callback { private final List<String> oldList; private final List<String> newList; public MyDiffCallback(List<String> oldList, List<String> newList) { this.oldList = oldList; this.newList = newList; } @Override public int getOldListSize() { return oldList.size(); } @Override public int getNewListSize() { return newList.size(); } @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { return oldList.get(oldItemPosition).equals(newList.get(newItemPosition)); } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { return oldList.get(oldItemPosition).equals(newList.get(newItemPosition)); } } // Step 2: Calculate Differences DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( new MyDiffCallback(oldList, newList) ); // Step 3: Dispatch Updates myAdapter.submitList(newList); diffResult.dispatchUpdatesTo(myAdapter);
✨ DiffUtils is a powerful tool for handling list updates in Android, ensuring efficient and minimal changes. By leveraging Myers’ Algorithm, it calculates the shortest edit script to transform one list into another. Understanding how it works not only improves your grasp of Android development but also helps you optimize RecyclerView performance.
💡Why Compose Doesn’t Need DiffUtil
Jetpack Compose is built on declarative UI principles, meaning you describe what your UI should look like based on the current state, and Compose takes care of the rest.
Imperative UI (Views):
- You manually update UI components by determining what changes need to be applied.
- Tools like DiffUtil are necessary to calculate the minimal updates for lists to keep performance in check.
Declarative UI (Compose):
- You describe what the UI should look like for a given state, not how to change it.
- Compose automatically re-composes only the parts of the UI impacted by state changes.
In Compose, the state drives the UI, and recomposition handles the updates. There’s no need for DiffUtil to calculate deltas because the system automatically optimizes what to re-render.
Here are the main reasons why Compose eliminates the need for DiffUtil
:
1. State-Driven UI 🔄
In Compose, the UI is automatically recomposed when the state changes. You don’t have to manually compute differences between lists; Compose handles it for you.
val items = remember { mutableStateListOf("Apple", "Banana", "Cherry") } LazyColumn { items(items) { item -> Text(text = item) } }
If you add or remove an item from items
, Compose will recompose only the affected parts of the UI. No DiffUtil
required! 🎯
2. Built-in Optimizations 🌐
Compose uses keys in LazyColumn
and LazyRow
to optimize item rendering. By specifying a unique key for each item, Compose can identify which items have changed, been added, or been removed.
LazyColumn { items(items = yourList, key = { item -> item.id }) { item -> Text(text = item.name) } }
The key
ensures that Compose efficiently updates only the affected items, similar to what DiffUtil
does.
3. Smart Recomposition 🔧
Compose intelligently skips recompositions for UI elements that haven’t changed. Using tools like remember
and rememberSaveable
, you can further optimize recomposition behavior.
@Composable fun RememberExample() { val count = remember { mutableStateOf(0) } Button(onClick = { count.value++ }) { Text("Clicked ${count.value} times") } } // Here, only the Text inside the Button recomposes when the state changes, // not the entire component.
Recomposition in Compose:
- Compose observes state changes. When the state of a particular UI element changes, only that element (and its dependencies) is recomposed.
- The system skips unchanged UI elements entirely.
DiffUtil in Views:
- Requires explicit calculation of changes between the old and new states of a list.
- The calculated changes are then dispatched to update the RecyclerView.
What to Use Instead of DiffUtil in Compose
While you don’t need DiffUtil
, Compose provides tools to achieve similar optimizations:
1. LazyColumn with Keys
Use key
to identify and manage changes in a list efficiently.
LazyColumn { items(items = yourList, key = { item -> item.id }) { item -> Text(text = item.name) } }
2. SnapshotStateList
For managing lists reactively, use SnapshotStateList
.
val items = remember { mutableStateListOf("Apple", "Banana", "Cherry") } Button(onClick = { items.add("Date") }) { Text("Add Item") } LazyColumn { items(items) { item -> Text(text = item) } }
Job Offers
3. SubcomposeLayout
For complex scenarios, SubcomposeLayout
provides precise control over what gets recomposed.
🌟 Interview Question
✍ 01. Why does Jetpack Compose not need
DiffUtil?
Compose relies on a declarative UI model. It automatically updates the UI based on state changes, eliminating the need for manually calculated list differences like DiffUtil
. Using keys in LazyColumn
ensures efficient updates without external tools.
✍ 02. How does Jetpack Compose handle list updates differently from
RecyclerView?
Instead of relying on manual difference calculation (DiffUtil
), Compose observes state changes and recomposes only the affected components. This is managed internally through the use of keys and Compose’s recomposition logic.
✍ 03. What are the benefits of declarative UI compared to imperative UI in Android development?
- Simpler Code: Declarative UI reduces boilerplate by focusing on the what instead of the how.
- Automatic State Management: Compose automatically updates the UI based on state changes.
- Improved Testability: Stateless composables can be tested independently.
- Consistency: Recomposition ensures UI always reflects the current state.
✍ 04. What is recomposition in Compose, and how does it differ from traditional view invalidation in
RecyclerView?
- Recomposition: Happens when Compose detects a state change. It regenerates only the parts of the UI affected by the change.
- View Invalidation: In
RecyclerView
, invalidation triggers a redraw of views, which can be inefficient without tools likeDiffUtil
.
✍ 05. When should you use
remember and
rememberSaveable in Compose?
- Use
remember
to store state during a single composition lifecycle. - Use
rememberSaveable
to retain state across configuration changes, like screen rotations.
✍ 06. How does Compose decide which parts of the UI to recompose?
Compose tracks state reads in each composable. When the state changes, only those composables reading the changed state are recomposed.
✍ 07. Implement a
LazyColumn in Compose to display a list of items and add a button to update the list. Ensure it updates efficiently.
@Composable fun LazyColumnExample() { val items = remember { mutableStateListOf("Apple", "Banana", "Cherry") } Column { Button(onClick = { items.add("Date") }) { Text("Add Item") } LazyColumn { items(items, key = { it }) { item -> Text(text = item) } } } }
✍ 08. Identify and fix unnecessary recompositions in a Compose component.
LazyColumn { items(items = list, key = { item -> item.id }) { item -> Text(text = item.name) } }
✍ 09. Diagnosing lag in a
LazyColumn:
- Check the key parameter: Ensure each item has a unique key.
- Use profiling tools: Analyze recomposition counts using Android Studio’s Compose Debugger.
- Optimize item rendering: Avoid heavy computations in the composable functions used within
LazyColumn
.
✍ 10. Debug inconsistent behavior in list updates:
Ensure the data source is stable and matches the UI expectations. Using SnapshotStateList
can help maintain reactivity.
✍ 11. Differences between
SnapshotStateList and
ArrayList:
SnapshotStateList
is reactive; changes automatically trigger recomposition in Compose.ArrayList
is not reactive and requires manual notifications for UI updates.
✍ 12. Using
SubcomposeLayout
SubcomposeLayout
is a powerful layout tool in Jetpack Compose that allows you to compose parts of a layout on demand. This is especially useful for cases where some parts of the UI are resource-intensive or may not be immediately available, like loading an image from the network or a database.SubcomposeLayout
allows composing parts of a layout on demand.
Example: dynamically loading an image and displaying a placeholder until it’s ready.
import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.rememberImagePainter import coil.compose.AsyncImage import androidx.compose.foundation.Image import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Alignment @Composable fun ImageWithPlaceholder(imageUrl: String, placeholder: Painter) { Box(modifier = Modifier.fillMaxSize()) { SubcomposeLayout { constraints -> // First, compose the placeholder val placeholderLayout = subcompose(0) { Image(painter = placeholder, contentDescription = null, modifier = Modifier.fillMaxSize()) } // Compose the image once it's loaded val imageLayout = subcompose(1) { AsyncImage( model = imageUrl, contentDescription = null, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) } // Return the max size for the layout layout(constraints.maxWidth, constraints.maxHeight) { placeholderLayout[0].measure(constraints).placeRelative(0, 0) imageLayout[0].measure(constraints).placeRelative(0, 0) } } } } @Preview @Composable fun ImageWithPlaceholderPreview() { ImageWithPlaceholder( imageUrl = "https://www.example.com/image.jpg", placeholder = painterResource(id = R.drawable.placeholder_image) ) }
- SubcomposeLayout: This layout allows you to compose parts of the layout on demand. Here, we are composing the placeholder first and then the image once it’s ready.
- subcompose(): This function is used to compose individual parts of the layout. The
subcompose
function returns a list ofMeasureResult
objects, which you can then measure and place on the screen. - AsyncImage: We use
AsyncImage
from thecoil-compose
library to load the image asynchronously. While it loads, the placeholder is shown. - Placeholder: The placeholder is displayed first. Once the image is ready, it takes over.
This method helps you create more efficient UIs by reducing unnecessary recomposition and handling dynamic content like images or data more gracefully.
✍ 13. Migrating a legacy
RecyclerView to Compose:
- Replace
RecyclerView
withLazyColumn
. - Move adapter logic into composable functions.
- Use
remember
orSnapshotStateList
for state management. - Optimize with keys.
✅ Conclusion
- Compose doesn’t need
DiffUtil
because it is built on a declarative and state-driven architecture. LazyColumn
withkey
andSnapshotStateList
provide similar optimizations.- Smart recomposition ensures efficient UI updates, reducing the need for manual optimizations.
By embracing Compose’s declarative nature, you can focus on building beautiful, responsive UIs without worrying about the complexities of list updates. 🎨
Happy Composing! 🚀
🌍 References
- Myers, E. (1986). An O(ND) Difference Algorithm and Its Variations. ACM Transactions on Programming Languages and Systems, 1(2), 251–266.
- Android Developer Documentation — DiffUtils
This article is previously published on proandroiddev.com.