Optimizing Performance
1. Using ViewBinding vs findViewById
| Feature | ViewBinding | findViewById |
|---|---|---|
| Type Safety | NEW Compile-time checked | Runtime errors possible |
| Performance | Fast, no reflection | Slower due to reflection |
| Memory Usage | Lightweight, no overhead | Minimal overhead |
| Boilerplate Code | Eliminates manual view lookups | Requires manual view lookups |
| Code Readability | Clear and concise | Verbose and repetitive |
Example: ViewBinding Usage
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.textView.text = "Hello World"
}
}
Example: findViewById Usage
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView = findViewById<TextView>(R.id.textView)
textView.text = "Hello World"
}
}
Note: ViewBinding is the recommended approach as it provides type safety and better performance compared to findViewById.
2. Implementing View Recycling
| Property | Description | Usage |
|---|---|---|
| RecyclerView | UI component that efficiently displays large datasets by recycling views | Use for scrollable lists or grids with dynamic content |
| ViewHolder | Container for view elements in RecyclerView | Created and reused by RecyclerView to minimize object creation |
| onBindViewHolder() | Method to bind data to a ViewHolder | Override to populate view elements with actual data |
Example: Basic RecyclerView Setup
class MyAdapter(private val dataList: List<String>) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.findViewById(R.id.textView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.textView.text = dataList[position]
}
override fun getItemCount(): Int = dataList.size
}
Note: View recycling prevents memory leaks and improves performance by reusing view instances instead of creating new ones for each item.
3. Reducing Layout Hierarchy
Minimizing the nesting level of views in your layout hierarchy is crucial for performance. Deeply nested layouts increase rendering time and memory usage.
| Layout | Description | Impact |
|---|---|---|
| LinearLayout | Vertical or horizontal arrangement of views | Can lead to deep nesting when used excessively |
| RelativeLayout | Views positioned relative to each other | Can be complex and harder to optimize |
| ConstraintLayout | Flexible layout with flat hierarchy | Better performance and easier to maintain |
Example: Using ConstraintLayout to reduce nesting
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Hello World"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/image"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Note: ConstraintLayout allows you to create complex layouts with a flat view hierarchy, which improves rendering performance.
4. Using ViewStub for Lazy Loading
ViewStub is a lightweight view that is initially invisible and can be inflated lazily when needed, helping to optimize performance by reducing initial layout overhead.
| Property | Description | Usage |
|---|---|---|
| android:layout | Resource ID of the layout to inflate when the ViewStub is inflated. | <ViewStub android:layout="@layout/expanded_layout" /> |
| android:inflatedId | Optional ID to assign to the inflated view. | <ViewStub android:inflatedId="@+id/expanded_view" /> |
| android:visibility | Always GONE until inflated; setting to VISIBLE or INVISIBLE will trigger inflation. |
<ViewStub android:visibility="gone" /> |
Example: Inflate ViewStub Programmatically
val viewStub = findViewById<ViewStub>(R.id.view_stub)
val inflatedView = viewStub.inflate()
Note: Inflating a ViewStub returns the inflated view, which can then be used to access child views or set listeners.
5. Optimizing Bitmap Loading
| Property | Description | Notes |
|---|---|---|
| BitmapFactory.Options | Options for decoding bitmap files | Used to control decoding behavior like scaling, inBitmap, etc. |
| inSampleSize | Scale factor for decoding bitmap | Reduces memory usage by loading a smaller version of the image |
| inBitmap | Reuses existing bitmap memory | Improves performance by avoiding allocation of new memory |
| inJustDecodeBounds | Decodes only bounds, not pixels | Useful for determining image size before loading |
Example: Loading a Scaled Bitmap
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
BitmapFactory.decodeResource(resources, R.drawable.image, this)
val height = outHeight
val width = outWidth
inSampleSize = calculateInSampleSize(this, 100, 100)
inJustDecodeBounds = false
}
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.image, options)
Example: Using inBitmap for Memory Reuse
val options = BitmapFactory.Options().apply {
inBitmap = reusedBitmap
}
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.image, options)
Note: Always use
inJustDecodeBounds first to determine image size before decoding.Warning:
inBitmap requires API level 19+ and compatible bitmap configurations.| Option | Values | Notes |
|---|---|---|
| inSampleSize | Integer (power of 2) | Defaults to 1, higher values reduce memory usage |
| inJustDecodeBounds | true / false |
Set to true to get image dimensions without decoding pixels |
| inBitmap | Bitmap? |
Must be same size and config as target bitmap |
6. Implementing Memory Caching
| Cache Type | Description | Implementation | Notes |
|---|---|---|---|
| LruCache | Least Recently Used cache that automatically evicts the least recently accessed items | val cache = LruCache<String, Bitmap>(maxSize) |
Use for memory-constrained environments |
| MemoryCache | Direct in-memory storage using HashMap or similar | val cache = mutableMapOf<String, Any>() |
Simple but requires manual eviction logic |
Example: Using LruCache
class BitmapCache : LruCache<String, Bitmap>(maxSize) {
override fun sizeOf(key: String, value: Bitmap): Int {
return value.byteCount
}
}
val cache = BitmapCache(1024 * 1024) // 1MB max size
Note: LruCache is thread-safe and suitable for UI rendering
Warning: Avoid caching large objects in memory to prevent OutOfMemoryError
7. Reducing Overdraw
| Concept | Description | Impact |
|---|---|---|
| Overdraw | When the GPU renders the same pixel multiple times in a single frame | Reduces performance and battery life |
| Causes | Multiple views with transparency, nested layouts, unused drawables | Increases rendering cost |
Example: Identifying Overdraw
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Example of inefficient nested layout
val parentLayout = findViewById<ViewGroup>(R.id.parent_layout)
val childView = findViewById<View>(R.id.child_view)
// This can cause overdraw if child_view has transparency
}
}
Techniques to Reduce Overdraw
- Use
android:backgroundinstead ofandroid:srcwhere possible - Avoid nested layouts with transparent elements
- Use
android:clipChildrento prevent drawing outside bounds - Enable "Show GPU Overdraw" in Developer Options to visualize issues
Note: Overdraw is more noticeable on older devices with less GPU power.
8. Using Strict Mode
StrictMode is a development tool that helps identify accidental performance issues in Android applications. It detects operations that are performed on the main thread and can cause ANR (Application Not Responding) errors.
| Feature | Description | Usage |
|---|---|---|
| detectDiskReads() | Detects disk reads on the main thread | StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().detectDiskReads().build()) |
| detectDiskWrites() | Detects disk writes on the main thread | StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().detectDiskWrites().build()) |
| detectNetwork() | Detects network calls on the main thread | StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().detectNetwork().build()) |
| detectAll() | Enables all checks | StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().detectAll().build()) |
Example: Enabling StrictMode
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
)
}
}
Note:
StrictMode should only be used during development and testing, not in production builds.
Warning: Enabling
StrictMode in production can lead to performance degradation and should be avoided.
9. Profiling with Android Profiler
Example: Launching Android Profiler
# From Android Studio
# Navigate to:
# Tools > Profiler
# Or use the shortcut:
# Shift + Alt + A (Windows/Linux)
# Shift + Option + A (macOS)
| Tool | Description | Use Case |
|---|---|---|
| CPU Profiler | Monitors thread activity, method calls, and CPU usage | Identify performance bottlenecks in code execution |
| Memory Profiler | Tracks object allocation, garbage collection, and memory leaks | Detect memory issues and optimize heap usage |
| Network Profiler | Captures network traffic and latency | Monitor API calls and optimize data transfer |
| Battery Profiler | Measures battery consumption of app components | Optimize power usage for better user experience |
Note: Android Profiler is part of Android Studio and provides real-time insights into app performance.
Workflow: Using Android Profiler
- Open Android Studio
- Navigate to
Tools > Profiler - Select the target device or emulator
- Choose a profiling tool (CPU, Memory, etc.)
- Start profiling and analyze the data
Key Features of Android Profiler
- Real-time data for CPU, memory, and network usage
- Thread analysis to identify blocking operations
- Memory snapshots for debugging leaks
- Battery impact tracking to optimize power consumption
10. Implementing LazyColumn Optimization
Example: Basic LazyColumn with Item Content
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun OptimizedLazyList(items: List<String>) {
LazyColumn {
items(items) { item ->
Text(text = item)
}
}
}
| Property | Description | Usage |
|---|---|---|
| items() | Function to load list items | Used inside LazyColumn to define item content |
| key() | Unique identifier for items | Improves performance by avoiding recomputation |
Note: Using
key is essential for performance when items can be reordered or updated.Example: LazyColumn with Key
LazyColumn {
items(items, key = { it.id }) { item ->
Text(text = item.name)
}
}
Performance Tips
- Use
keyfor stable item identifiers - Avoid recomputing items unnecessarily
- Use
LazyColumnfor large lists to reduce memory usage
11. Avoiding Memory Leaks
| Problem | Solution | Notes |
|---|---|---|
| Context references | WeakReference or SoftReference |
Prevent holding strong references to contexts in long-lived objects |
| Static references | Use WeakHashMap or remove static references |
Static fields can cause memory leaks if they hold context or view references |
| Broadcast receivers | Unregister in onDestroy() |
Leak if not unregistered, especially in Activities or Fragments |
| Async tasks | Cancel in onDestroy() |
Async tasks can hold references to Activity or Fragment |
| Closures in views | Use ViewTreeObserver or Handler.post() |
Can cause leaks if not properly detached |
Example: Using WeakReference to avoid context leak
class MyActivity : AppCompatActivity() {
private val contextRef = WeakReference<Context>(this)
fun doSomething() {
val context = contextRef.get()
if (context != null) {
// Safe to use context here
}
}
}
Example: Unregistering broadcast receiver
class MyActivity : AppCompatActivity() {
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
registerReceiver(receiver, IntentFilter("ACTION"))
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(receiver) // Prevents leak
}
}
Note: Memory leaks in Android often occur due to long-lived references to Activities or Contexts. Always ensure that listeners, observers, and callbacks are properly cleaned up.
12. Using Baseline Profiles
Baseline Profiles help optimize Android app performance by providing runtime information about typical user workflows. This data is used to guide the Android Runtime (ART) in optimizing code execution.
| Profile Type | Description | Usage |
|---|---|---|
| Baseline Profile | Contains performance data collected from typical app usage scenarios. | Used by ART to optimize code at runtime. |
| App Profile | Includes detailed information on method calls and execution paths. | Used for performance tuning and optimization. |
Example: Baseline Profile Configuration
android {
compileSdk 34
defaultConfig {
targetSdk 34
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
buildConfig = true
}
}
Example: Enabling Baseline Profiles in Gradle
android {
compileSdk 34
baselineProfile {
automaticGeneration = true
}
}
Note: Baseline Profiles are generated during app builds and can be used to optimize performance for typical user workflows.