The Pillars of Robust Android Applications : Part 3
In the previous parts of this series, we explored how to design a secured (Part 2) Android applications. In this part, we are going to…
The Pillars of Robust Android Applications : Part 3
In the previous parts of this series, we explored how to design a secured (Part 2) Android applications. In this part, we are going to understand how adhering to very basic knowledge and coding principle, can improve your app performance real-time. As Android developers, we spend countless hours designing beautiful UIs, implementing new features, and fixing bugs. But no matter how feature-rich an application is, users will remember one thing above everything else — how it feels.

An app that stutters while scrolling, takes a few extra seconds to launch, hangs a bit while using, quickly leaves the impression of being unpolished. In today’s app ecosystem, performance isn’t just a technical metric — it’s part of the user experience.
You might have already noticed:
- Poor performance drains battery, leading to user frustration & ultimately uninstalling the app
- Users abandon apps that take more than 3 seconds to start initially
- Google Play Store ranks faster apps higher
- Optimized apps work smoothly on budget devices too
If you’re reading this article, you probably already have a basic understanding of what can impact an app’s performance. One of the biggest misconceptions, however, is that performance problems are primarily caused by “slow devices.” In reality, many bottlenecks originate from the code we write every day. Excessive or unnecessary recompositions that cause more UI work than required.
- Creating objects during composition, leading to frequent allocations and garbage collection
- Performing expensive/heavy work on the main thread
- Memory leaks caused by improperly scoped objects, listeners, or references that outlive their lifecycle.
- Inefficient state management, resulting in larger recomposition scopes than necessary.
- decoding large images that too during recomposition
- Using deep or nested UI hierarchies, increasing layout and rendering costs.
Individually, these issues may seem insignificant, and the application may continue to work as per requirements, but together they consume valuable CPU time, increase memory stacks and compilation, leading to non-seamless experience in hands of user.
Performance issues in “real” compose screen
Imagine you’re building a simple social feed, or an e-commerce product list. The implementation below follows Compose conventions, uses a LazyColumn, loads images, and displays a list of cards. At first glance, there's nothing obviously wrong with it.
@Composable
fun FeedScreen(
posts: List<Post>
) {
LazyColumn {
items(posts) { post ->
FeedItem(post)
}
}
}
@Composable
fun FeedItem(
post: Post
) {
val formatter = SimpleDateFormat(
"dd MMM yyyy",
Locale.getDefault()
)
val formattedDate =
formatter.format(Date(post.createdAt))
//reading time for specific post
val readingTime = post.description
.split(" ")
.size / 200
val imageRequest = ImageRequest.Builder(
LocalContext.current
)
.data(post.image)
.crossfade(true)
.build()
//fetch likes for specific post
LaunchedEffect(Unit) {
repository.fetchLikes(post.id)
}
Card(
modifier = Modifier.padding(12.dp)
) {
Column {
AsyncImage(
model = imageRequest,
contentDescription = null,
modifier = Modifier.height(220.dp)
)
Text("$readingTime min read")
Text(post.author)
Text(post.description)
Text(formattedDate)
}
}
}
Everything looks perfectly reasonable. We have a screen that displays a list of feed items, where each card contains an author’s name, a description, date, and an image. The implementation follows ComposeLazyColumn that renders the list. The code compiles successfully, with no syntax errors or warnings, and the UI renders exactly as expected.
However, performance issues are often overlooked, they hide behind code that looks completely harmless. As the column scrolls, or a piece of state changes, the system performs work that may seem insignificant, but when dealt with large number of items or repeated multiple times in fraction of second, it begins to consume valuable CPU time, increase memory allocations, and triggering garbage collection.
The result is a screen that looks correct but doesn’t feel smooth to users.
Issue #1 :A Single Object Created Hundreds of Times
val formatter = SimpleDateFormat(
"dd MMM yyyy",
Locale.getDefault()
)
Consider if the feed has 100 items, a new instance is created every time FeedItem is executed. In a LazyColumn, items are composed as they enter the viewport and may be recomposed whenever their observed state changes. So the SimpleDateFormat object gets allocated in memory and as the user scrolls, each item that enters the viewport gets recreated and re-allocated. Eventually the heap grows in system memory, due to large repeated allocations and garbage collection is run, increasing the likelihood of dropped frames during scrolling.
Solution: If formatting truly belongs in the UI layer (for example, because it depends on the current locale or user preferences), remember the formatter:
val formatter = remember {
SimpleDateFormat(
"dd MMM yyyy",
Locale.getDefault()
)
}
This is better because, no new formatter on every recomposition
Issue #2 : ImageRequest Being Recreated
As mentioned earlier, composable functions can execute many times throughout their lifecycle. Whenever FeedItem recomposes, due to state changes, its parent recomposes, or the item is newly composed as it enters the viewport—a new ImageRequest object is created.
val imageRequest = ImageRequest.Builder(LocalContext.current)
.data(post.imageUrl)
.crossfade(true)
.build()
If an object depends only on stable inputs and doesn’t need to be recreated on every composition, either
rememberit or avoid creating it altogether.
Issue #3: Missing Stable Keys
LazyColumn {
items(posts) { post ->
FeedItem(post)
}
}
Compose keeps track of states with List’s unique identity (keys), without which it cannot determine which item corresponds to which previous composition. Instead of updating just the new item in list of 100 feed items, it may dispose and recreate many existing compositions, causing unnecessary recomposition and layout work.
Solution:
LazyColumn {
items(
items = posts,
// state for recomp
key = { it.id }
) { post ->
FeedItem(post)
}
}
Issue #4: Business Logic Inside Composition
@Composable
fun FeedItem(post: Post) {
val readingTime = post.description
.split(" ")
.size / 200
Text("$readingTime min read")
}
It is advisable to write all business logics in view-model so that when your screen recomposes, the main thread is not burdened with lot of computation each and every time. In this case, if the feed has 100 items, and each post description contains 1000+ words, for which reading time is calculated inside FeedItem. As the user scolls rapidly, the UI can be seen flickering as the compution is happening on main thread for each and every item.
Solution: create a UiState in view-model that can survive with same state as long as view-model instance is thriving in memory.
data class PostUi(
val readingTime: Int
)
Issue #5 : Network Calls in LauchedEfffect()
LaunchedEffect(Unit) {
repository.fetchLikes(post.id)
}
However, remember how
LazyColumnworks:
1.Items are composed as they enter the viewport.
- Items leaving the viewport are disposed.
- When the user scrolls back, those items are composed again.
This issue oftem makes the UI very slow, as API call might not be on main thread but its heavy task. if you plan to keep a loader while the API call is success or error, but loader for each and every item API calls might not look good in UI.
Solution: Best way is to maintain a UiState for all parameters that we need to show in UI, which is declared in viewmodel. The responsibility of UI is only to display the values.
data class PostUiModel(
val id: String,
val author: String,
val description: String,
val image: String,
val formattedDate: String,
val likes: Int
)
Why Does My App Feel Slow?
The challenge is that some mistakes might not be able to stand out during development. The app compiles, the UI looks correct, and everything appears to work. But beneath the surface, Android is fighting to render every frame within a strict deadline.
At 60 Hz, the display refreshes every 16.67 milliseconds. If your application takes even slightly longer than that to prepare the next frame, the frame is delayed until the next refresh cycle.
What are the findings ?
Here is summary of all the issues we discussed above, so that next time you create a screen, you think how your screen will perform on a real user’s device.
- Slow rendering is mostly on the Main screen where we want to display app/product potentials in several type of views.
- Unnecessary onLayout, onMeasure & onDraw calls.
- Lots of initialization in the main thread.
- Stable keys in LazyColumn allow Compose to preserve item identity and avoid unnecessary recomposition and layout work when the list changes.
- There are many threads created, each new worker thread takes considerable amount of time and resources.
- Repetition of few operations which can be otherwise avoided by saving them into constants and reuse those values.
- Deep nesting increases the work required during the Measure and Layout phases of every frame.
- Performing main-thread heavy work, like sorting, filtering, or JSON parsing, in background thread in View-model.
Although every app has its own reasons for drop in performance and own solutions to fix it, but considering above points will definitely give you the direction to debug your app and make a better app.
If you have any questions, suggestions, or even disagreements, feel free to drop them in the comments — I’d love to discuss! And if this helped you, don’t forget to leave a clap👏
메타데이터
- post_id
- 4de9ae2d8176
- slug
- the-pillars-of-robust-android-applications-part-3-4de9ae2d8176
- url
- https://medium.com/@pratistha_05/the-pillars-of-robust-android-applications-part-3-4de9ae2d8176
- canonical_url
- https://medium.com/@pratistha_05/the-pillars-of-robust-android-applications-part-3-4de9ae2d8176
- author_url
- https://medium.com/@pratistha_05
- status
- ok
- fetched_at
- 2026-08-02 07:24:00