November 22, 2025
20 min read

Senior Mobile Developer (Android) Interview Questions: Complete Guide

interview
career-advice
job-search
Senior Mobile Developer (Android) Interview Questions: Complete Guide
MB

Milad Bonakdar

Author

Master advanced Android development with essential interview questions covering architecture patterns, performance optimization, dependency injection, testing, security, and system design for senior developers.


Introduction

Senior Android developers are expected to architect scalable, maintainable applications while ensuring high performance and code quality. This role demands deep expertise in Android frameworks, architectural patterns, dependency injection, testing strategies, and the ability to make informed technical decisions.

This comprehensive guide covers essential interview questions for Senior Android Developers, spanning advanced Kotlin concepts, architectural patterns, performance optimization, dependency injection, testing, and system design. Each question includes detailed answers, rarity assessment, and difficulty ratings.


Advanced Kotlin & Language Features (5 Questions)

1. Explain Kotlin Coroutines and their advantages over threads.

Answer: Coroutines are lightweight concurrency primitives that allow writing asynchronous code in a sequential manner.

  • Advantages over Threads:
    • Lightweight: Can create thousands of coroutines without performance issues
    • Structured Concurrency: Parent-child relationship ensures proper cleanup
    • Cancellation Support: Built-in cancellation propagation
    • Exception Handling: Structured exception handling
  • Key Components:
    • CoroutineScope: Defines lifecycle
    • Dispatchers: Control execution context (Main, IO, Default)
    • suspend functions: Can be paused and resumed
class UserRepository(private val api: ApiService) {
    suspend fun getUser(id: Int): Result<User> = withContext(Dispatchers.IO) {
        try {
            val user = api.fetchUser(id)
            Result.success(user)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

class UserViewModel : ViewModel() {
    private val _user = MutableLiveData<User>()
    val user: LiveData<User> = _user
    
    fun loadUser(id: Int) {
        viewModelScope.launch {
            when (val result = repository.getUser(id)) {
                is Result.Success -> _user.value = result.data
                is Result.Error -> handleError(result.exception)
            }
        }
    }
}

Rarity: Very Common Difficulty: Hard


2. What are Sealed Classes and when should you use them?

Answer: Sealed classes represent restricted class hierarchies where all subclasses are known at compile time.

  • Benefits:
    • Exhaustive when expressions
    • Type-safe state management
    • Better than enums for complex data
  • Use Cases: Representing states, results, navigation events
sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String) : UiState<Nothing>()
}

class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState<User>>(UiState.Loading)
    val uiState: StateFlow<UiState<User>> = _uiState.asStateFlow()
    
    fun loadUser() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                val user = repository.getUser()
                _uiState.value = UiState.Success(user)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

// In UI
when (val state = uiState.value) {
    is UiState.Loading -> showLoading()
    is UiState.Success -> showData(state.data)
    is UiState.Error -> showError(state.message)
    // Compiler ensures all cases are handled
}

Rarity: Common Difficulty: Medium


3. Explain Kotlin Flow and how it differs from LiveData.

Answer: Flow is Kotlin's cold asynchronous stream that emits values sequentially.

  • Flow vs LiveData:
    • Flow: Cold stream, supports operators, not lifecycle-aware, more flexible
    • LiveData: Hot stream, lifecycle-aware, Android-specific, simpler for UI
  • Flow Types:
    • Flow: Cold stream (starts on collection)
    • StateFlow: Hot stream with current state
    • SharedFlow: Hot stream for events
class UserRepository {
    // Cold Flow - starts when collected
    fun getUsers(): Flow<List<User>> = flow {
        val users = api.fetchUsers()
        emit(users)
    }.flowOn(Dispatchers.IO)
    
    // StateFlow - hot, holds state
    private val _userState = MutableStateFlow<List<User>>(emptyList())
    val userState: StateFlow<List<User>> = _userState.asStateFlow()
    
    // SharedFlow - hot, for events
    private val _events = MutableSharedFlow<Event>()
    val events: SharedFlow<Event> = _events.asSharedFlow()
}

class UserViewModel : ViewModel() {
    val users = repository.getUsers()
        .map { it.filter { user -> user.isActive } }
        .catch { emit(emptyList()) }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
}

// Collect in Activity/Fragment
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.users.collect { users ->
            updateUI(users)
        }
    }
}

Rarity: Very Common Difficulty: Hard


4. What are Inline Functions and when should you use them?

Answer: Inline functions copy the function body to the call site, avoiding function call overhead.

  • Benefits:
    • Eliminates lambda allocation overhead
    • Allows non-local returns from lambdas
    • Better performance for higher-order functions
  • Use Cases: Higher-order functions with lambda parameters
  • Trade-off: Increases code size
// Without inline - creates lambda object
fun measureTime(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    val end = System.currentTimeMillis()
    println("Time: ${end - start}ms")
}

// With inline - no lambda object created
inline fun measureTimeInline(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    val end = System.currentTimeMillis()
    println("Time: ${end - start}ms")
}

// noinline - prevents specific parameter from being inlined
inline fun performOperation(
    inline operation: () -> Unit,
    noinline logger: () -> Unit
) {
    operation()
    saveLogger(logger)  // Can store noinline lambda
}

// crossinline - allows inline but prevents non-local returns
inline fun runAsync(crossinline block: () -> Unit) {
    thread {
        block()  // Can't return from outer function
    }
}

Rarity: Medium Difficulty: Hard


5. Explain Delegation in Kotlin.

Answer: Delegation allows an object to delegate some of its responsibilities to another object.

  • Class Delegation: by keyword
  • Property Delegation: Lazy, observable, delegates
  • Benefits: Code reuse, composition over inheritance
// Class delegation
interface Repository {
    fun getData(): String
}

class RemoteRepository : Repository {
    override fun getData() = "Remote data"
}

class CachedRepository(
    private val remote: Repository
) : Repository by remote {
    private var cache: String? = null
    
    override fun getData(): String {
        return cache ?: remote.getData().also { cache = it }
    }
}

// Property delegation
class User {
    // Lazy initialization
    val database by lazy {
        Room.databaseBuilder(context, AppDatabase::class.java, "db").build()
    }
    
    // Observable property
    var name: String by Delegates.observable("Initial") { prop, old, new ->
        println("$old -> $new")
    }
    
    // Custom delegate
    var token: String by PreferenceDelegate("auth_token", "")
}

class PreferenceDelegate(
    private val key: String,
    private val defaultValue: String
) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return sharedPreferences.getString(key, defaultValue) ?: defaultValue
    }
    
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        sharedPreferences.edit().putString(key, value).apply()
    }
}

Rarity: Medium Difficulty: Medium


Architecture Patterns (6 Questions)

6. Explain MVVM architecture and its benefits.

Answer: MVVM (Model-View-ViewModel) separates UI logic from business logic.

Loading diagram...
  • Model: Data layer (repositories, data sources)
  • View: UI layer (Activities, Fragments, Composables)
  • ViewModel: Presentation logic, survives configuration changes
  • Benefits: Testable, separation of concerns, lifecycle-aware
// Model
data class User(val id: Int, val name: String, val email: String)

// Repository
class UserRepository(
    private val api: ApiService,
    private val dao: UserDao
) {
    suspend fun getUser(id: Int): User {
        return dao.getUser(id) ?: api.fetchUser(id).also {
            dao.insertUser(it)
        }
    }
}

// ViewModel
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow<UiState<User>>(UiState.Loading)
    val uiState: StateFlow<UiState<User>> = _uiState.asStateFlow()
    
    fun loadUser(id: Int) {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                val user = repository.getUser(id)
                _uiState.value = UiState.Success(user)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "Error")
            }
        }
    }
}

// View (Fragment)
class UserFragment : Fragment() {
    private val viewModel: UserViewModel by viewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.uiState.collect { state ->
                when (state) {
                    is UiState.Loading -> showLoading()
                    is UiState.Success -> showUser(state.data)
                    is UiState.Error -> showError(state.message)
                }
            }
        }
    }
}

Rarity: Very Common Difficulty: Medium


7. What is Clean Architecture and how do you implement it in Android?

Answer: Clean Architecture separates code into layers with clear dependencies.

Loading diagram...
  • Presentation: UI, ViewModels
  • Domain: Use Cases, Business Logic, Entities
  • Data: Repositories, Data Sources (API, Database)
  • Dependency Rule: Inner layers don't know about outer layers
// Domain Layer - Entities
data class User(val id: Int, val name: String, val email: String)

// Domain Layer - Repository Interface
interface UserRepository {
    suspend fun getUser(id: Int): Result<User>
}

// Domain Layer - Use Case
class GetUserUseCase(private val repository: UserRepository) {
    suspend operator fun invoke(id: Int): Result<User> {
        return repository.getUser(id)
    }
}

// Data Layer - Repository Implementation
class UserRepositoryImpl(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource
) : UserRepository {
    override suspend fun getUser(id: Int): Result<User> {
        return try {
            val localUser = localDataSource.getUser(id)
            if (localUser != null) {
                Result.success(localUser)
            } else {
                val remoteUser = remoteDataSource.fetchUser(id)
                localDataSource.saveUser(remoteUser)
                Result.success(remoteUser)
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

// Presentation Layer - ViewModel
class UserViewModel(
    private val getUserUseCase: GetUserUseCase
) : ViewModel() {
    fun loadUser(id: Int) {
        viewModelScope.launch {
            when (val result = getUserUseCase(id)) {
                is Result.Success -> handleSuccess(result.data)
                is Result.Failure -> handleError(result.exception)
            }
        }
    }
}

Rarity: Common Difficulty: Hard


8. Explain Dependency Injection and Dagger/Hilt.

Answer: Dependency Injection provides dependencies to classes instead of creating them internally.

  • Benefits: Testability, loose coupling, reusability
  • Dagger: Compile-time DI framework
  • Hilt: Simplified Dagger for Android
// Define dependencies with Hilt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    
    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService {
        return retrofit.create(ApiService::class.java)
    }
}

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(context, AppDatabase::class.java, "app_db")
            .build()
    }
    
    @Provides
    fun provideUserDao(database: AppDatabase): UserDao {
        return database.userDao()
    }
}

// Repository with injected dependencies
@Singleton
class UserRepository @Inject constructor(
    private val apiService: ApiService,
    private val userDao: UserDao
) {
    suspend fun getUser(id: Int): User {
        return userDao.getUser(id) ?: apiService.fetchUser(id).also {
            userDao.insertUser(it)
        }
    }
}

// ViewModel with injected repository
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {
    // ViewModel logic
}

// Activity/Fragment
@AndroidEntryPoint
class UserActivity : AppCompatActivity() {
    private val viewModel: UserViewModel by viewModels()
}

Rarity: Very Common Difficulty: Hard


9. What is the Repository pattern and why use it?

Answer: Repository pattern abstracts data sources, providing a clean API for data access.

  • Benefits:
    • Single source of truth
    • Centralized data logic
    • Easy to switch data sources
    • Testable
  • Implementation: Coordinates between multiple data sources
interface UserRepository {
    suspend fun getUsers(): Flow<List<User>>
    suspend fun getUser(id: Int): User?
    suspend fun saveUser(user: User)
    suspend fun deleteUser(id: Int)
}

class UserRepositoryImpl @Inject constructor(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource,
    private val cacheDataSource: UserCacheDataSource
) : UserRepository {
    
    override suspend fun getUsers(): Flow<List<User>> = flow {
        // Emit cached data first
        val cachedUsers = cacheDataSource.getUsers()
        if (cachedUsers.isNotEmpty()) {
            emit(cachedUsers)
        }
        
        // Fetch from local database
        val localUsers = localDataSource.getUsers()
        if (localUsers.isNotEmpty()) {
            emit(localUsers)
            cacheDataSource.saveUsers(localUsers)
        }
        
        // Fetch from remote
        try {
            val remoteUsers = remoteDataSource.fetchUsers()
            localDataSource.saveUsers(remoteUsers)
            cacheDataSource.saveUsers(remoteUsers)
            emit(remoteUsers)
        } catch (e: Exception) {
            // If remote fails, we already emitted cached/local data
        }
    }
    
    override suspend fun getUser(id: Int): User? {
        return cacheDataSource.getUser(id)
            ?: localDataSource.getUser(id)
            ?: remoteDataSource.fetchUser(id)?.also {
                localDataSource.saveUser(it)
                cacheDataSource.saveUser(it)
            }
    }
}

Rarity: Very Common Difficulty: Medium


10. Explain the Single Activity architecture.

Answer: Single Activity architecture uses one Activity with multiple Fragments, managed by Navigation Component.

  • Benefits:
    • Simplified navigation
    • Shared ViewModels between fragments
    • Better animations
    • Easier deep linking
  • Navigation Component: Handles fragment transactions, back stack, arguments
// Navigation graph (nav_graph.xml)
<navigation>
    <fragment
        android:id="@+id/homeFragment"
        android:name="com.app.HomeFragment">
        <action
            android:id="@+id/action_home_to_detail"
            app:destination="@id/detailFragment" />
    </fragment>
    
    <fragment
        android:id="@+id/detailFragment"
        android:name="com.app.DetailFragment">
        <argument
            android:name="userId"
            app:argType="integer" />
    </fragment>
</navigation>

// MainActivity - single activity
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        val navController = findNavController(R.id.nav_host_fragment)
        setupActionBarWithNavController(navController)
    }
}

// HomeFragment - navigate to detail
class HomeFragment : Fragment() {
    fun navigateToDetail(userId: Int) {
        val action = HomeFragmentDirections.actionHomeToDetail(userId)
        findNavController().navigate(action)
    }
}

// DetailFragment - receive arguments
class DetailFragment : Fragment() {
    private val args: DetailFragmentArgs by navArgs()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val userId = args.userId
        viewModel.loadUser(userId)
    }
}

Rarity: Common Difficulty: Medium


11. What is MVI (Model-View-Intent) architecture?

Answer: MVI is a unidirectional data flow architecture inspired by Redux.

  • Components:
    • Model: Represents UI state
    • View: Renders state, emits intents
    • Intent: User actions/events
  • Benefits: Predictable state, easier debugging, time-travel debugging
// State
data class UserScreenState(
    val isLoading: Boolean = false,
    val user: User? = null,
    val error: String? = null
)

// Intent (user actions)
sealed class UserIntent {
    data class LoadUser(val id: Int) : UserIntent()
    object RetryLoading : UserIntent()
}

// ViewModel
class UserViewModel : ViewModel() {
    private val _state = MutableStateFlow(UserScreenState())
    val state: StateFlow<UserScreenState> = _state.asStateFlow()
    
    fun processIntent(intent: UserIntent) {
        when (intent) {
            is UserIntent.LoadUser -> loadUser(intent.id)
            is UserIntent.RetryLoading -> retryLoading()
        }
    }
    
    private fun loadUser(id: Int) {
        viewModelScope.launch {
            _state.value = _state.value.copy(isLoading = true, error = null)
            try {
                val user = repository.getUser(id)
                _state.value = _state.value.copy(
                    isLoading = false,
                    user = user
                )
            } catch (e: Exception) {
                _state.value = _state.value.copy(
                    isLoading = false,
                    error = e.message
                )
            }
        }
    }
}

// View
class UserFragment : Fragment() {
    private val viewModel: UserViewModel by viewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // Send intent
        viewModel.processIntent(UserIntent.LoadUser(123))
        
        // Observe state
        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.state.collect { state ->
                render(state)
            }
        }
    }
    
    private fun render(state: UserScreenState) {
        when {
            state.isLoading -> showLoading()
            state.error != null -> showError(state.error)
            state.user != null -> showUser(state.user)
        }
    }
}

Rarity: Medium Difficulty: Hard


Performance & Optimization (5 Questions)

12. How do you optimize RecyclerView performance?

Answer: Multiple strategies improve RecyclerView scrolling performance:

  1. ViewHolder Pattern: Reuse views (built-in)
  2. DiffUtil: Efficient list updates
  3. Stable IDs: Override getItemId() and setHasStableIds(true)
  4. Prefetching: Increase prefetch distance
  5. Image Loading: Use libraries like Glide/Coil with proper sizing
  6. Avoid Heavy Operations: Don't perform expensive calculations in onBindViewHolder
  7. Nested RecyclerViews: Set setRecycledViewPool() and setHasFixedSize(true)
class UserAdapter : ListAdapter<User, UserViewHolder>(UserDiffCallback()) {
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val binding = ItemUserBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return UserViewHolder(binding)
    }
    
    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        holder.bind(getItem(position))
    }
    
    override fun getItemId(position: Int): Long {
        return getItem(position).id.toLong()
    }
}

class UserDiffCallback : 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
    }
}

// In Fragment/Activity
recyclerView.apply {
    setHasFixedSize(true)
    adapter = userAdapter
    // Increase prefetch
    (layoutManager as? LinearLayoutManager)?.initialPrefetchItemCount = 4
}

// Update list efficiently
userAdapter.submitList(newUsers)

Rarity: Very Common Difficulty: Medium


13. How do you detect and fix memory leaks in Android?

Answer: Memory leaks occur when objects are held in memory longer than needed.

  • Common Causes:
    • Context leaks (Activity/Fragment references)
    • Static references
    • Anonymous inner classes
    • Listeners not unregistered
    • Coroutines not cancelled
  • Detection Tools:
    • LeakCanary library
    • Android Studio Memory Profiler
    • Heap dumps
// BAD - Memory leak
class MyActivity : AppCompatActivity() {
    companion object {
        var instance: MyActivity? = null  // Static reference leaks
    }
    
    private val handler = Handler()  // Implicit reference to Activity
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        instance = this
        
        // Anonymous class holds Activity reference
        handler.postDelayed({
            // Activity might be destroyed
        }, 10000)
    }
}

// GOOD - No memory leak
class MyActivity : AppCompatActivity() {
    private val handler = Handler(Looper.getMainLooper())
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Use weak reference if needed
        val weakRef = WeakReference(this)
        handler.postDelayed({
            weakRef.get()?.let { activity ->
                // Safe to use activity
            }
        }, 10000)
    }
    
    override fun onDestroy() {
        super.onDestroy()
        handler.removeCallbacksAndMessages(null)  // Clean up
    }
}

// ViewModel - survives configuration changes
class MyViewModel : ViewModel() {
    private val job = Job()
    private val scope = CoroutineScope(Dispatchers.Main + job)
    
    override fun onCleared() {
        super.onCleared()
        job.cancel()  // Cancel coroutines
    }
}

Rarity: Very Common Difficulty: Medium


14. How do you optimize app startup time?

Answer: Faster startup improves user experience:

  1. Lazy Initialization: Initialize objects only when needed
  2. Avoid Heavy Work in Application.onCreate():
    • Move to background thread
    • Defer non-critical initialization
  3. Content Providers: Minimize or lazy-load
  4. Reduce Dependencies: Fewer libraries = faster startup
  5. App Startup Library: Structured initialization
  6. Baseline Profiles: Ahead-of-time compilation hints
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        // Critical initialization only
        initCrashReporting()
        
        // Defer non-critical work
        lifecycleScope.launch {
            delay(100)
            initAnalytics()
            initImageLoader()
        }
        
        // Background initialization
        CoroutineScope(Dispatchers.IO).launch {
            preloadData()
        }
    }
}

// App Startup library
class AnalyticsInitializer : Initializer<Analytics> {
    override fun create(context: Context): Analytics {
        return Analytics.getInstance(context).apply {
            initialize()
        }
    }
    
    override fun dependencies(): List<Class<out Initializer<*>>> {
        return emptyList()
    }
}

// Lazy initialization
class MyActivity : AppCompatActivity() {
    private val database by lazy {
        Room.databaseBuilder(this, AppDatabase::class.java, "db").build()
    }
    
    // Only created when accessed
    private val heavyObject by lazy {
        createHeavyObject()
    }
}

Rarity: Common Difficulty: Medium


15. How do you handle bitmap loading and caching efficiently?

Answer: Efficient image handling is crucial for performance:

  • Libraries: Glide, Coil (handle caching automatically)
  • Manual Optimization:
    • Downsampling (load smaller images)
    • Memory cache (LruCache)
    • Disk cache
    • Bitmap pooling
// Using Glide
Glide.with(context)
    .load(imageUrl)
    .placeholder(R.drawable.placeholder)
    .error(R.drawable.error)
    .diskCacheStrategy(DiskCacheStrategy.ALL)
    .override(width, height)  // Resize
    .into(imageView)

// Manual implementation with LruCache
class ImageCache {
    private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
    private val cacheSize = maxMemory / 8
    
    private val memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
        override fun sizeOf(key: String, bitmap: Bitmap): Int {
            return bitmap.byteCount / 1024
        }
    }
    
    fun getBitmap(key: String): Bitmap? {
        return memoryCache.get(key)
    }
    
    fun putBitmap(key: String, bitmap: Bitmap) {
        if (getBitmap(key) == null) {
            memoryCache.put(key, bitmap)
        }
    }
}

// Downsample large images
fun decodeSampledBitmap(file: File, reqWidth: Int, reqHeight: Int): Bitmap {
    return BitmapFactory.Options().run {
        inJustDecodeBounds = true
        BitmapFactory.decodeFile(file.path, this)
        
        inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
        inJustDecodeBounds = false
        
        BitmapFactory.decodeFile(file.path, this)
    }
}

fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
    val (height: Int, width: Int) = options.run { outHeight to outWidth }
    var inSampleSize = 1
    
    if (height > reqHeight || width > reqWidth) {
        val halfHeight: Int = height / 2
        val halfWidth: Int = width / 2
        
        while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
            inSampleSize *= 2
        }
    }
    
    return inSampleSize
}

Rarity: Common Difficulty: Hard


16. What is ANR and how do you prevent it?

Answer: ANR (Application Not Responding) occurs when the main thread is blocked for too long.

  • Causes:
    • Heavy computation on main thread
    • Network calls on main thread
    • Database operations on main thread
    • Deadlocks
  • Prevention:
    • Move heavy work to background threads
    • Use coroutines with proper dispatchers
    • Avoid synchronized blocks on main thread
    • Use WorkManager for background tasks
// BAD - Blocks main thread
class BadActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // ANR risk!
        val data = fetchDataFromNetwork()  // Blocks UI thread
        processLargeFile()  // Blocks UI thread
    }
}

// GOOD - Background processing
class GoodActivity : AppCompatActivity() {
    private val viewModel: MyViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        viewModel.loadData()  // Handled in ViewModel with coroutines
    }
}

class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            // Automatically on Main dispatcher
            showLoading()
            
            // Switch to IO dispatcher for network
            val data = withContext(Dispatchers.IO) {
                fetchDataFromNetwork()
            }
            
            // Switch to Default dispatcher for heavy computation
            val processed = withContext(Dispatchers.Default) {
                processData(data)
            }
            
            // Back to Main for UI update
            updateUI(processed)
        }
    }
}

Rarity: Common Difficulty: Easy


Testing (3 Questions)

17. How do you write unit tests for ViewModels?

Answer: ViewModels should be tested in isolation with mocked dependencies.

class UserViewModelTest {
    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()
    
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()
    
    private lateinit var viewModel: UserViewModel
    private lateinit var repository: UserRepository
    
    @Before
    fun setup() {
        repository = mockk()
        viewModel = UserViewModel(repository)
    }
    
    @Test
    fun `loadUser success updates state`() = runTest {
        // Given
        val user = User(1, "John", "john@example.com")
        coEvery { repository.getUser(1) } returns Result.success(user)
        
        // When
        viewModel.loadUser(1)
        
        // Then
        val state = viewModel.uiState.value
        assertTrue(state is UiState.Success)
        assertEquals(user, (state as UiState.Success).data)
    }
    
    @Test
    fun `loadUser failure updates error state`() = runTest {
        // Given
        val exception = Exception("Network error")
        coEvery { repository.getUser(1) } returns Result.failure(exception)
        
        // When
        viewModel.loadUser(1)
        
        // Then
        val state = viewModel.uiState.value
        assertTrue(state is UiState.Error)
        assertEquals("Network error", (state as UiState.Error).message)
    }
}

// MainDispatcherRule for testing coroutines
@ExperimentalCoroutinesApi
class MainDispatcherRule : TestWatcher() {
    private val testDispatcher = StandardTestDispatcher()
    
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }
    
    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

Rarity: Very Common Difficulty: Medium


18. What is the difference between Unit, Integration, and UI tests?

Answer:

  • Unit Tests:
    • Test individual components in isolation
    • Fast, run on JVM
    • Mock dependencies
    • Example: ViewModel, Repository, Use Case tests
  • Integration Tests:
    • Test how components work together
    • May require Android framework
    • Example: Repository with real database
  • UI Tests (Instrumented):
    • Test user interactions
    • Run on device/emulator
    • Slower but verify actual behavior
    • Example: Espresso tests
// Unit test - JVM
class UserRepositoryTest {
    @Test
    fun `getUser returns cached user`() {
        val cache = mockk<UserCache>()
        val api = mockk<ApiService>()
        val repository = UserRepository(api, cache)
        
        every { cache.getUser(1) } returns User(1, "John", "john@example.com")
        
        val user = runBlocking { repository.getUser(1) }
        
        assertEquals("John", user.name)
        verify(exactly = 0) { api.fetchUser(any()) }  // API not called
    }
}

// Integration test - Android
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
    private lateinit var database: AppDatabase
    private lateinit var userDao: UserDao
    
    @Before
    fun setup() {
        database = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            AppDatabase::class.java
        ).build()
        userDao = database.userDao()
    }
    
    @Test
    fun insertAndGetUser() = runBlocking {
        val user = User(1, "John", "john@example.com")
        userDao.insertUser(user)
        
        val retrieved = userDao.getUser(1)
        assertEquals(user, retrieved)
    }
}

// UI test - Espresso
@RunWith(AndroidJUnit4::class)
class UserActivityTest {
    @get:Rule
    val activityRule = ActivityScenarioRule(UserActivity::class.java)
    
    @Test
    fun displayUserName() {
        onView(withId(R.id.nameText))
            .check(matches(withText("John Doe")))
        
        onView(withId(R.id.loadButton))
            .perform(click())
        
        onView(withId(R.id.progressBar))
            .check(matches(isDisplayed()))
    }
}

Rarity: Common Difficulty: Easy


19. How do you test Coroutines and Flow?

Answer: Use testing libraries designed for coroutines.

class FlowTest {
    @Test
    fun `flow emits correct values`() = runTest {
        val flow = flow {
            emit(1)
            delay(100)
            emit(2)
            delay(100)
            emit(3)
        }
        
        val values = flow.toList()
        assertEquals(listOf(1, 2, 3), values)
    }
    
    @Test
    fun `stateFlow updates correctly`() = runTest {
        val stateFlow = MutableStateFlow(0)
        
        val job = launch {
            stateFlow.emit(1)
            stateFlow.emit(2)
        }
        
        advanceUntilIdle()  // Process all coroutines
        assertEquals(2, stateFlow.value)
        job.cancel()
    }
    
    @Test
    fun `turbine library for flow testing`() = runTest {
        val flow = flowOf(1, 2, 3)
        
        flow.test {
            assertEquals(1, awaitItem())
            assertEquals(2, awaitItem())
            assertEquals(3, awaitItem())
            awaitComplete()
        }
    }
}

Rarity: Common Difficulty: Medium


Security & Best Practices (2 Questions)

20. How do you secure sensitive data in Android?

Answer: Multiple layers protect app and user data:

  1. EncryptedSharedPreferences: Encrypt preferences
  2. Keystore: Secure key storage
  3. Network Security: HTTPS, certificate pinning
  4. ProGuard/R8: Code obfuscation
  5. Root Detection: Detect compromised devices
// EncryptedSharedPreferences
val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()

val encryptedPrefs = EncryptedSharedPreferences.create(
    context,
    "secure_prefs",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

encryptedPrefs.edit()
    .putString("auth_token", token)
    .apply()

// Keystore for encryption keys
val keyGenerator = KeyGenerator.getInstance(
    KeyProperties.KEY_ALGORITHM_AES,
    "AndroidKeyStore"
)

val keyGenParameterSpec = KeyGenParameterSpec.Builder(
    "MyKeyAlias",
    KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    .build()

keyGenerator.init(keyGenParameterSpec)
val secretKey = keyGenerator.generateKey()

// Certificate pinning with OkHttp
val certificatePinner = CertificatePinner.Builder()
    .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .build()

val client = OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build()

Rarity: Common Difficulty: Hard


21. What are Android best practices for production apps?

Answer: Key practices for production-ready apps:

  1. Architecture: Use MVVM/Clean Architecture
  2. Dependency Injection: Hilt for testability
  3. Error Handling: Proper exception handling, user-friendly messages
  4. Logging: Structured logging, crash reporting (Firebase Crashlytics)
  5. Testing: Unit, integration, and UI tests
  6. Performance: Monitor with Firebase Performance
  7. Security: Encrypt sensitive data, use HTTPS
  8. Offline Support: Cache data, handle network errors
  9. Accessibility: Support TalkBack, content descriptions
  10. Localization: Support multiple languages

Rarity: Common Difficulty: Medium


Related Posts

Recent Posts

Weekly career tips that actually work

Get the latest insights delivered straight to your inbox