Skip to main content

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:background instead of android:src where possible
  • Avoid nested layouts with transparent elements
  • Use android:clipChildren to 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:
# &nbsp;&nbsp;&nbsp;&nbsp;Tools &gt; Profiler

# Or use the shortcut:
# &nbsp;&nbsp;&nbsp;&nbsp;Shift + Alt + A (Windows/Linux)
# &nbsp;&nbsp;&nbsp;&nbsp;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

  1. Open Android Studio
  2. Navigate to Tools &gt; Profiler
  3. Select the target device or emulator
  4. Choose a profiling tool (CPU, Memory, etc.)
  5. 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 key for stable item identifiers
  • Avoid recomputing items unnecessarily
  • Use LazyColumn for 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.

Was this article helpful?