Ritesh Singh logo
Ritesh Sohlot
Ritesh Sohlot

Android Performance Optimization: Complete Guide to Building Fast Apps

Learn essential techniques for optimizing Android app performance, including memory management, UI optimization, network efficiency, and battery life improvements.

Published
Reading Time
8 min read
Views
0 views

Performance optimization is crucial for creating Android applications that provide a smooth user experience. Poor performance can lead to user frustration, negative reviews, and decreased engagement. This comprehensive guide will walk you through essential techniques for optimizing your Android applications.

Performance Metrics

Key Performance Indicators (KPIs)

  • App Launch Time: Time from tap to first frame
  • Frame Rate: Maintain 60 FPS for smooth animations
  • Memory Usage: Efficient memory management
  • Battery Consumption: Minimize battery drain
  • Network Efficiency: Optimize data usage
  • APK Size: Reduce app size for faster downloads

Performance Profiling Tools

  • Android Profiler: Built-in profiling in Android Studio
  • Systrace: System-level performance analysis
  • Perfetto: Advanced tracing and analysis
  • LeakCanary: Memory leak detection
  • Firebase Performance: Real-world performance monitoring

Memory Management

Memory Leaks Prevention

class MainActivity : AppCompatActivity() {
    
    // ❌ Bad: Potential memory leak
    private val handler = Handler()
    
    // ✅ Good: Use weak references
    private val handler = Handler(Looper.getMainLooper())
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // ❌ Bad: Anonymous inner class holds activity reference
        handler.postDelayed(object : Runnable {
            override fun run() {
                updateUI() // This can cause memory leaks
            }
        }, 1000)
        
        // ✅ Good: Use weak reference or clear handler
        handler.postDelayed({
            if (!isFinishing) {
                updateUI()
            }
        }, 1000)
    }
    
    override fun onDestroy() {
        super.onDestroy()
        // ✅ Good: Remove callbacks to prevent memory leaks
        handler.removeCallbacksAndMessages(null)
    }
}

Efficient Data Structures

// ❌ Bad: Inefficient for large datasets
val userList = ArrayList<User>()

// ✅ Good: Use appropriate data structures
val userMap = HashMap<Int, User>() // For frequent lookups
val userSet = HashSet<User>() // For unique collections

// ✅ Good: Use LazyColumn for large lists
@Composable
fun UserList(users: List<User>) {
    LazyColumn {
        items(users) { user ->
            UserItem(user = user)
        }
    }
}

Object Pooling

class ObjectPool<T>(
    private val maxSize: Int,
    private val factory: () -> T
) {
    private val pool = ConcurrentLinkedQueue<T>()
    
    fun acquire(): T {
        return pool.poll() ?: factory()
    }
    
    fun release(obj: T) {
        if (pool.size < maxSize) {
            pool.offer(obj)
        }
    }
}

// Usage
val bitmapPool = ObjectPool(10) { Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) }

UI Performance Optimization

View Optimization

// ❌ Bad: Nested layouts cause performance issues
<LinearLayout>
    <LinearLayout>
        <LinearLayout>
            <TextView />
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

// ✅ Good: Use ConstraintLayout for complex layouts
<androidx.constraintlayout.widget.ConstraintLayout>
    <TextView
        android:id="@+id/textView"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

RecyclerView Optimization

class OptimizedAdapter : RecyclerView.Adapter<ViewHolder>() {
    
    // ✅ Good: ViewHolder pattern
    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val textView: TextView = itemView.findViewById(R.id.textView)
        private val imageView: ImageView = itemView.findViewById(R.id.imageView)
        
        fun bind(user: User) {
            textView.text = user.name
            // Use image loading library
            Glide.with(itemView.context)
                .load(user.avatar)
                .into(imageView)
        }
    }
    
    // ✅ Good: DiffUtil for efficient updates
    private val diffCallback = object : DiffUtil.ItemCallback<User>() {
        override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
            return oldItem.id == newItem.id
        }
        
        override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
            return oldItem == newItem
        }
    }
    
    private val differ = AsyncListDiffer(this, diffCallback)
}

Compose Performance

// ✅ Good: Use remember and derivedStateOf
@Composable
fun OptimizedUserList(users: List<User>) {
    val filteredUsers by remember(users) {
        derivedStateOf {
            users.filter { it.isActive }
        }
    }
    
    LazyColumn {
        items(filteredUsers) { user ->
            UserItem(user = user)
        }
    }
}

// ✅ Good: Avoid expensive operations in composables
@Composable
fun ExpensiveComposable(data: List<String>) {
    val processedData = remember(data) {
        // Expensive processing
        data.map { it.uppercase() }
    }
    
    LazyColumn {
        items(processedData) { item ->
            Text(text = item)
        }
    }
}

Network Optimization

Efficient API Calls

class NetworkOptimizer {
    
    // ✅ Good: Use pagination
    suspend fun getUsers(page: Int, limit: Int = 20): List<User> {
        return apiService.getUsers(page = page, limit = limit)
    }
    
    // ✅ Good: Implement caching
    suspend fun getUsersWithCache(): List<User> {
        return try {
            val users = apiService.getUsers()
            userDao.insertUsers(users) // Cache
            users
        } catch (e: Exception) {
            userDao.getAllUsers() // Fallback to cache
        }
    }
    
    // ✅ Good: Use appropriate timeouts
    private val okHttpClient = OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(30, TimeUnit.SECONDS)
        .build()
}

Image Loading Optimization

// ✅ Good: Efficient image loading
object ImageLoader {
    
    fun loadImage(
        imageView: ImageView,
        url: String,
        placeholder: Int = R.drawable.placeholder
    ) {
        Glide.with(imageView.context)
            .load(url)
            .placeholder(placeholder)
            .error(placeholder)
            .diskCacheStrategy(DiskCacheStrategy.ALL)
            .into(imageView)
    }
    
    fun preloadImages(context: Context, urls: List<String>) {
        urls.forEach { url ->
            Glide.with(context)
                .load(url)
                .diskCacheStrategy(DiskCacheStrategy.ALL)
                .preload()
        }
    }
}

Battery Optimization

Background Processing

// ✅ Good: Use WorkManager for background tasks
class DataSyncWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {
    
    override suspend fun doWork(): Result {
        return try {
            // Perform background work
            syncData()
            Result.success()
        } catch (e: Exception) {
            if (runAttemptCount < 3) {
                Result.retry()
            } else {
                Result.failure()
            }
        }
    }
}

// Schedule background work
val syncRequest = PeriodicWorkRequestBuilder<DataSyncWorker>(
    15, TimeUnit.MINUTES
).setConstraints(
    Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .setRequiresBatteryNotLow(true)
        .build()
).build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "data_sync",
    ExistingPeriodicWorkPolicy.KEEP,
    syncRequest
)

Location Services Optimization

class LocationManager(private val context: Context) {
    
    private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
    
    fun requestLocationUpdates() {
        val locationRequest = LocationRequest.create().apply {
            priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY
            interval = 30000 // 30 seconds
            fastestInterval = 10000 // 10 seconds
        }
        
        fusedLocationClient.requestLocationUpdates(
            locationRequest,
            locationCallback,
            Looper.getMainLooper()
        )
    }
    
    private val locationCallback = object : LocationCallback() {
        override fun onLocationResult(locationResult: LocationResult) {
            locationResult.lastLocation?.let { location ->
                // Handle location update
            }
        }
    }
}

APK Size Optimization

Code Optimization

// ✅ Good: Enable R8/ProGuard
android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

// ProGuard rules
-keep class com.example.model.** { *; }
-keepclassmembers class com.example.model.** {
    <fields>;
}

Resource Optimization

// ✅ Good: Use vector drawables
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
    <path
        android:fillColor="#FF000000"
        android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2z"/>
</vector>

// ✅ Good: Use WebP images
// Convert PNG/JPG to WebP for smaller file sizes

Database Optimization

Room Database Optimization

@Database(
    entities = [User::class, Post::class],
    version = 1,
    exportSchema = false
)
abstract class OptimizedDatabase : RoomDatabase() {
    
    abstract fun userDao(): UserDao
    abstract fun postDao(): PostDao
    
    companion object {
        @Volatile
        private var INSTANCE: OptimizedDatabase? = null
        
        fun getDatabase(context: Context): OptimizedDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    OptimizedDatabase::class.java,
                    "optimized_database"
                )
                .setQueryCallback({ sqlQuery, bindArgs ->
                    // Log slow queries
                    if (sqlQuery.contains("SELECT") && sqlQuery.length > 100) {
                        Log.d("Database", "Slow query: $sqlQuery")
                    }
                }, Executors.newSingleThreadExecutor())
                .build()
                INSTANCE = instance
                instance
            }
        }
    }
}

Efficient Queries

@Dao
interface OptimizedUserDao {
    
    // ✅ Good: Use indices for frequently queried columns
    @Query("SELECT * FROM users WHERE email = :email LIMIT 1")
    suspend fun getUserByEmail(email: String): User?
    
    // ✅ Good: Use pagination for large datasets
    @Query("SELECT * FROM users LIMIT :limit OFFSET :offset")
    suspend fun getUsersPaginated(limit: Int, offset: Int): List<User>
    
    // ✅ Good: Use transactions for multiple operations
    @Transaction
    suspend fun insertUserWithPosts(user: User, posts: List<Post>) {
        insertUser(user)
        insertPosts(posts)
    }
}

Startup Optimization

Application Startup

@HiltAndroidApp
class OptimizedApplication : Application() {
    
    override fun onCreate() {
        super.onCreate()
        
        // ✅ Good: Initialize heavy components lazily
        initializeComponents()
    }
    
    private fun initializeComponents() {
        // Use background thread for heavy initialization
        CoroutineScope(Dispatchers.IO).launch {
            // Initialize database
            // Initialize network components
            // Initialize analytics
        }
    }
}

Activity Startup

class OptimizedActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        // ✅ Good: Enable hardware acceleration
        window.setFlags(
            WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
            WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
        )
        
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // ✅ Good: Defer non-critical initialization
        lifecycleScope.launch {
            initializeNonCriticalComponents()
        }
    }
    
    private suspend fun initializeNonCriticalComponents() {
        withContext(Dispatchers.IO) {
            // Initialize analytics
            // Load cached data
            // Preload images
        }
    }
}

Memory Profiling

Memory Leak Detection

// ✅ Good: Use LeakCanary for memory leak detection
dependencies {
    debugImplementation "com.squareup.leakcanary:leakcanary-android:2.12"
}

// Custom memory leak detection
class MemoryLeakDetector {
    
    private val weakReferences = mutableListOf<WeakReference<Any>>()
    
    fun trackObject(obj: Any) {
        weakReferences.add(WeakReference(obj))
    }
    
    fun checkForLeaks() {
        val leakedObjects = weakReferences.filter { it.get() != null }
        if (leakedObjects.isNotEmpty()) {
            Log.w("MemoryLeak", "Potential memory leaks detected: ${leakedObjects.size}")
        }
    }
}

Performance Monitoring

Custom Performance Tracking

class PerformanceTracker {
    
    private val metrics = mutableMapOf<String, Long>()
    
    fun startTimer(tag: String) {
        metrics[tag] = System.currentTimeMillis()
    }
    
    fun endTimer(tag: String) {
        val startTime = metrics[tag]
        if (startTime != null) {
            val duration = System.currentTimeMillis() - startTime
            Log.d("Performance", "$tag took ${duration}ms")
            metrics.remove(tag)
        }
    }
    
    fun trackMemoryUsage() {
        val runtime = Runtime.getRuntime()
        val usedMemory = runtime.totalMemory() - runtime.freeMemory()
        val maxMemory = runtime.maxMemory()
        val memoryUsage = (usedMemory.toFloat() / maxMemory.toFloat()) * 100
        
        Log.d("Performance", "Memory usage: ${memoryUsage}%")
    }
}

Best Practices

1. UI Thread Optimization

  • Keep UI thread free for rendering
  • Use background threads for heavy operations
  • Avoid blocking operations on main thread

2. Memory Management

  • Use appropriate data structures
  • Implement object pooling for frequently created objects
  • Avoid memory leaks with proper lifecycle management

3. Network Optimization

  • Implement caching strategies
  • Use pagination for large datasets
  • Optimize image loading and caching

4. Battery Optimization

  • Use WorkManager for background tasks
  • Implement efficient location services
  • Minimize wake locks and background processing

5. APK Size Optimization

  • Enable code shrinking and obfuscation
  • Use vector drawables and WebP images
  • Remove unused resources and dependencies

6. Database Optimization

  • Use appropriate indices
  • Implement efficient queries
  • Use transactions for multiple operations

7. Startup Optimization

  • Defer non-critical initialization
  • Use lazy loading for heavy components
  • Enable hardware acceleration

Performance Testing

Benchmark Testing

@RunWith(AndroidJUnit4::class)
class PerformanceTest {
    
    @Test
    fun testAppLaunchTime() {
        val startTime = System.currentTimeMillis()
        
        // Launch app
        val scenario = ActivityScenario.launch(MainActivity::class.java)
        
        scenario.onActivity { activity ->
            // Wait for app to be fully loaded
            Thread.sleep(1000)
        }
        
        val endTime = System.currentTimeMillis()
        val launchTime = endTime - startTime
        
        // Assert launch time is acceptable
        assertThat(launchTime).isLessThan(3000) // 3 seconds
    }
    
    @Test
    fun testMemoryUsage() {
        val runtime = Runtime.getRuntime()
        val initialMemory = runtime.totalMemory() - runtime.freeMemory()
        
        // Perform memory-intensive operation
        repeat(1000) {
            // Create objects
        }
        
        val finalMemory = runtime.totalMemory() - runtime.freeMemory()
        val memoryIncrease = finalMemory - initialMemory
        
        // Assert memory increase is reasonable
        assertThat(memoryIncrease).isLessThan(50 * 1024 * 1024) // 50MB
    }
}

Conclusion

Performance optimization is an ongoing process that requires continuous monitoring and improvement. By implementing these techniques, you can create Android applications that:

  • Launch quickly and respond to user interactions
  • Use memory efficiently without leaks
  • Conserve battery through optimized background processing
  • Load data efficiently with proper caching and pagination
  • Maintain smooth animations with optimized UI rendering

Remember to:

  • Profile your app regularly using Android Profiler
  • Monitor real-world performance with Firebase Performance
  • Test on low-end devices to ensure broad compatibility
  • Optimize incrementally based on user feedback and metrics
  • Balance performance with code maintainability

By following these optimization strategies, you can build fast, efficient Android applications that provide an excellent user experience across all devices.

Article completed • Thank you for reading!