Toolmingo
Guides26 min read

50 Android Interview Questions (With Answers)

Top Android interview questions with clear answers and code examples — covering Kotlin, Jetpack Compose, architecture patterns, Coroutines, and the Android lifecycle.

Android interviews test lifecycle knowledge, Kotlin proficiency, Jetpack Compose, architecture patterns, and performance. This guide covers the 50 most common questions with clear answers and code examples.

Quick reference

Topic Most asked questions
Lifecycle Activity/Fragment lifecycle, ViewModel survival
Kotlin Coroutines, Flow, data class, sealed class
Jetpack Compose State, recomposition, LaunchedEffect, side effects
Architecture MVVM, Clean Architecture, Repository pattern
Jetpack Room, Navigation, WorkManager, DataStore
Performance Memory leaks, ANR, RecyclerView optimisation

Android fundamentals

1. What is the Activity lifecycle? List all callbacks in order.

onCreate() → onStart() → onResume() → [Running]
                                          ↓
                                       onPause()
                                          ↓
                                       onStop()
                                          ↓
                               onRestart() or onDestroy()
Callback When called State after
onCreate() Activity is first created Created
onStart() Activity becomes visible Started
onResume() Activity gains focus Resumed
onPause() Another activity takes focus Paused
onStop() Activity is no longer visible Stopped
onRestart() Stopped activity is coming back Restarted
onDestroy() Activity is being destroyed Destroyed
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Initialize UI, set up ViewModel
    }

    override fun onResume() {
        super.onResume()
        // Register listeners, start animations
    }

    override fun onPause() {
        super.onPause()
        // Unregister listeners, pause animations, save draft state
    }
}

2. What is the Fragment lifecycle and how does it differ from Activity?

Fragment lifecycle is more complex — it has both a fragment lifecycle and a view lifecycle.

onAttach() → onCreate() → onCreateView() → onViewCreated()
→ onViewStateRestored() → onStart() → onResume()
→ onPause() → onStop() → onSaveInstanceState()
→ onDestroyView() → onDestroy() → onDetach()

Key difference: Fragment has onCreateView() / onDestroyView() for its view. The view can be destroyed (back stack) while the fragment instance stays alive — so never store view references beyond onDestroyView().

class MyFragment : Fragment(R.layout.fragment_my) {
    // Use viewLifecycleOwner for LiveData, not 'this'
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.data.observe(viewLifecycleOwner) { data ->
            // safe — tied to view lifecycle
        }
    }
}

3. What is ViewModel and why does it survive configuration changes?

ViewModel survives configuration changes (rotation, dark mode toggle) because it is stored in a ViewModelStore attached to the Activity's non-configuration instance — which Android retains across configuration changes.

class CounterViewModel : ViewModel() {
    private val _count = MutableStateFlow(0)
    val count: StateFlow<Int> = _count.asStateFlow()

    fun increment() {
        _count.update { it + 1 }
    }

    override fun onCleared() {
        // Called when owning Activity/Fragment is permanently destroyed
        // Cancel any ongoing work here
    }
}

// In Activity/Fragment
class CounterActivity : AppCompatActivity() {
    private val viewModel: CounterViewModel by viewModels()
}

ViewModel should not hold references to Context, View, or any Activity — that causes memory leaks.


4. What is the difference between savedInstanceState and ViewModel for state persistence?

savedInstanceState (Bundle) ViewModel
Survives rotation Yes Yes
Survives process death Yes (serialised to disk) No
Data size limit ~1 MB (TransactionTooLargeException) Unlimited (in memory)
Supports complex objects Only Parcelable/Serializable Any type
Scope Single Activity/Fragment instance Activity/Fragment + back stack

Best practice: Use ViewModel for in-memory state + data. Use savedInstanceState (or SavedStateHandle in ViewModel) for UI state that must survive process death.

class MyViewModel(private val state: SavedStateHandle) : ViewModel() {
    // Automatically persisted via savedInstanceState
    var searchQuery: String by state.saveable { "" }
}

5. What is an Intent? Explain explicit vs. implicit intents.

An Intent is a message object used to request an action from another component.

// Explicit Intent — specific component
val intent = Intent(this, DetailActivity::class.java).apply {
    putExtra("item_id", 42)
}
startActivity(intent)

// Implicit Intent — declare action, let the system find a component
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://toolko.com"))
if (intent.resolveActivity(packageManager) != null) {
    startActivity(intent)
}

// Share text
val shareIntent = Intent.createChooser(
    Intent(Intent.ACTION_SEND).apply {
        type = "text/plain"
        putExtra(Intent.EXTRA_TEXT, "Check this out!")
    },
    "Share via"
)
startActivity(shareIntent)

6. What is an IntentFilter?

IntentFilter declares what implicit intents a component can handle. Declared in AndroidManifest.xml.

<activity android:name=".ShareActivity">
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="text/plain" />
    </intent-filter>
</activity>

For security, sensitive activities should use explicit intents and not export themselves unnecessarily (set android:exported="false").


7. What is the difference between Service, IntentService, and WorkManager?

Service IntentService (deprecated) WorkManager
Runs on Main thread (by default) Background thread Background thread
Queue requests No Yes (serial) Yes
Survives app close Yes (if started) Yes Yes
Respects battery No No Yes (Doze-aware)
Scheduling No No Yes (constraints, delay)
// WorkManager — recommended for deferrable background work
val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .build()

val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>()
    .setConstraints(constraints)
    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.SECONDS)
    .build()

WorkManager.getInstance(context).enqueue(uploadRequest)

8. What is BroadcastReceiver and when should you use it?

BroadcastReceiver listens for system-wide or app-specific events.

// Dynamic receiver — registered in code, unregistered in onPause/onDestroy
class BatteryReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
        val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
        val batteryPct = level * 100 / scale.toFloat()
        Log.d("Battery", "Level: $batteryPct%")
    }
}

// Register
val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
registerReceiver(batteryReceiver, filter)

// Unregister in onPause/onDestroy
unregisterReceiver(batteryReceiver)

Avoid static receivers in manifest for implicit broadcasts (restricted since Android 8.0).


Kotlin for Android

9. What is the difference between val and var? What is const val?

val name = "Alice"        // immutable reference (like Java final)
var count = 0             // mutable reference

const val MAX_SIZE = 100  // compile-time constant (top-level or in companion object)
                          // must be String or primitive type

val doesn't mean the object is immutable — the reference is immutable:

val list = mutableListOf(1, 2, 3)
list.add(4)   // OK — list contents changed, reference didn't
list = mutableListOf()  // Error — can't reassign val

10. What is a data class? What does the compiler generate?

data class auto-generates: equals(), hashCode(), toString(), copy(), and componentN() functions.

data class User(val id: Int, val name: String, val email: String)

val alice = User(1, "Alice", "alice@example.com")
val alice2 = alice.copy(email = "newalice@example.com")

// Destructuring (uses componentN)
val (id, name, email) = alice

// Equals by value
println(alice == User(1, "Alice", "alice@example.com"))  // true

Rules: must have at least one parameter in primary constructor; all parameters should be val/var; cannot be abstract, open, sealed, or inner.


11. What is a sealed class and when is it used?

sealed class restricts class hierarchies — all subclasses must be defined in the same package (Kotlin 1.5+, previously same file).

sealed class UiState<out T> {
    data object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String) : UiState<Nothing>()
}

// Exhaustive when — no else needed
fun render(state: UiState<List<User>>) = when (state) {
    is UiState.Loading -> showSpinner()
    is UiState.Success -> showUsers(state.data)
    is UiState.Error -> showError(state.message)
}

12. Explain Kotlin extension functions.

Extension functions add methods to existing classes without modifying their source.

// Add a method to String
fun String.toTitleCase(): String =
    split(" ").joinToString(" ") { word ->
        word.replaceFirstChar { it.uppercase() }
    }

println("hello world".toTitleCase())  // Hello World

// Extension on Android View
fun View.visible() { visibility = View.VISIBLE }
fun View.gone() { visibility = View.GONE }

// Usage
myButton.visible()
myProgressBar.gone()

Extension functions are resolved statically at compile time — they don't truly modify the class.


13. What is the difference between object, companion object, and class?

// object — singleton
object Database {
    fun query(sql: String): String = "result"
}
Database.query("SELECT *")   // No instantiation needed

// companion object — static-like members on a class
class MyFragment : Fragment() {
    companion object {
        private const val ARG_ID = "id"

        fun newInstance(id: Int) = MyFragment().apply {
            arguments = bundleOf(ARG_ID to id)
        }
    }
}
val fragment = MyFragment.newInstance(42)

// class — regular class, requires instantiation
class User(val name: String)
val user = User("Alice")

Kotlin Coroutines

14. What is a Coroutine? How does it differ from a Thread?

A coroutine is a suspendable computation — it can pause at suspend points and resume later without blocking a thread.

Feature Thread Coroutine
Memory ~1 MB stack ~1 KB heap
Blocking Blocks OS thread Suspends (releases thread)
Switching OS context switch Application-level suspension
Number Hundreds Millions
Cancellation Thread.interrupt() Structured cancellation
// Thread — blocks
thread {
    Thread.sleep(1000)   // blocks thread
    println("Done")
}

// Coroutine — suspends
viewModelScope.launch {
    delay(1000)          // suspends, not blocks
    println("Done")
}

15. What are suspend functions?

A suspend function can be paused and resumed. It can only be called from a coroutine or another suspend function.

// Suspend function — can call other suspend functions
suspend fun fetchUser(id: Int): User {
    val response = api.getUser(id)      // network call, non-blocking
    return response.body()!!
}

// Usage in coroutine
viewModelScope.launch {
    val user = fetchUser(42)             // suspends here if needed
    _uiState.value = UiState.Success(user)
}

// Under the hood: the compiler transforms suspend functions
// into state machines (CPS transformation)

16. What is CoroutineScope and what built-in scopes does Android provide?

CoroutineScope defines the lifetime of coroutines launched within it.

Scope Lifecycle Use case
viewModelScope Until ViewModel.onCleared() ViewModel operations
lifecycleScope Until Activity/Fragment destroyed UI operations
repeatOnLifecycle Active only in given state Collecting flows safely
GlobalScope App lifetime Rare — avoid in most cases
class UserViewModel : ViewModel() {
    fun loadUser(id: Int) {
        viewModelScope.launch {
            try {
                val user = repo.getUser(id)
                _uiState.value = UiState.Success(user)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

// Collect Flow safely — only when UI is at least STARTED
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state -> render(state) }
    }
}

17. What is Flow and how does it differ from LiveData?

Flow is a cold, asynchronous stream from Kotlin Coroutines. LiveData is Android-specific and lifecycle-aware by default.

Feature Flow LiveData
Framework Kotlin (no Android dep) Android Jetpack
Cold/Hot Cold (starts on collect) Hot (always active)
Threading Any dispatcher Main thread
Operators Rich (map, filter, combine, …) Limited
Lifecycle awareness Manual (repeatOnLifecycle) Built-in
Testing turbine library InstantTaskExecutorRule
// Repository returns Flow
fun getUsers(): Flow<List<User>> = flow {
    while (true) {
        emit(api.fetchUsers())
        delay(30_000)  // poll every 30s
    }
}.catch { e -> emit(emptyList()) }
 .flowOn(Dispatchers.IO)

// ViewModel exposes StateFlow
val users: StateFlow<List<User>> = repo.getUsers()
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())

18. What is the difference between StateFlow and SharedFlow?

StateFlow SharedFlow
Initial value Required None
Replays last value to new collectors Always (1 item) Configurable (replay)
Use case UI state Events, one-shot messages
// StateFlow — for state
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()

// SharedFlow — for events (navigate, show snackbar)
private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()

fun triggerNavigation() {
    viewModelScope.launch {
        _events.emit(UiEvent.NavigateToDetail(id = 42))
    }
}

Jetpack Compose

19. What is Jetpack Compose and how does it differ from the View system?

Jetpack Compose is Android's modern declarative UI toolkit.

Feature View System Jetpack Compose
UI paradigm Imperative (XML + code) Declarative (Kotlin functions)
State updates Manual (setText, setVisibility) Automatic recomposition
UI tree ViewGroup hierarchy Composable tree
Theming XML styles/themes MaterialTheme, CompositionLocal
Animation Animator, XML animate*AsState, AnimatedContent
Testing Espresso Compose Test API (onNodeWith*)
@Composable
fun UserCard(user: User, onFollow: () -> Unit) {
    Card(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
        Row(
            modifier = Modifier.padding(16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            AsyncImage(
                model = user.avatarUrl,
                contentDescription = "Avatar of ${user.name}",
                modifier = Modifier.size(48.dp).clip(CircleShape)
            )
            Spacer(Modifier.width(12.dp))
            Column(Modifier.weight(1f)) {
                Text(user.name, style = MaterialTheme.typography.titleMedium)
                Text(user.email, style = MaterialTheme.typography.bodySmall)
            }
            Button(onClick = onFollow) { Text("Follow") }
        }
    }
}

20. What is recomposition? How do you optimise it?

Recomposition is the process of re-executing composable functions when their state changes.

// Bad — whole UserList recomposes when any item changes
@Composable
fun UserList(users: List<User>) {
    Column {
        users.forEach { user ->
            UserItem(user)   // all recompose on any change
        }
    }
}

// Good — LazyColumn with stable keys
@Composable
fun UserList(users: List<User>) {
    LazyColumn {
        items(users, key = { it.id }) { user ->
            UserItem(user)   // only changed items recompose
        }
    }
}

// Stability — mark classes as stable/immutable for compose optimiser
@Immutable
data class User(val id: Int, val name: String)

// Use remember to avoid re-creating objects on recomposition
@Composable
fun MyComposable() {
    val formatter = remember { DateTimeFormatter.ofPattern("dd MMM yyyy") }
}

21. What are side effects in Compose? Explain LaunchedEffect, SideEffect, DisposableEffect.

Side effects in Compose are operations that escape the composable's scope.

// LaunchedEffect — run a coroutine when key changes
@Composable
fun SearchScreen(query: String) {
    LaunchedEffect(query) {       // re-launches when query changes
        delay(300)                // debounce
        viewModel.search(query)
    }
}

// SideEffect — runs after every successful recomposition
@Composable
fun AnalyticsScreen(screenName: String) {
    SideEffect {
        analytics.trackScreen(screenName)  // synchronous, no coroutine
    }
}

// DisposableEffect — runs on enter, cleanup on exit
@Composable
fun LocationTracker() {
    DisposableEffect(Unit) {
        val listener = LocationListener { ... }
        locationManager.requestUpdates(listener)
        onDispose {
            locationManager.removeUpdates(listener)   // cleanup
        }
    }
}

22. How does state hoisting work in Compose?

State hoisting moves state up to a caller to make composables stateless (reusable and testable).

// Stateless — receives state and event handler
@Composable
fun SearchBar(
    query: String,
    onQueryChange: (String) -> Unit,
    modifier: Modifier = Modifier
) {
    OutlinedTextField(
        value = query,
        onValueChange = onQueryChange,
        modifier = modifier,
        label = { Text("Search") }
    )
}

// Stateful — owns state (used at the top level)
@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
    val query by viewModel.query.collectAsStateWithLifecycle()
    SearchBar(
        query = query,
        onQueryChange = viewModel::setQuery
    )
}

23. What is remember vs rememberSaveable?

@Composable
fun CounterScreen() {
    // remember — survives recomposition, NOT configuration changes
    var count by remember { mutableStateOf(0) }

    // rememberSaveable — survives recomposition AND configuration changes
    // uses Bundle under the hood — only works with Saver-compatible types
    var name by rememberSaveable { mutableStateOf("") }

    // Custom Saver for complex types
    var user by rememberSaveable(stateSaver = UserSaver) { mutableStateOf(User()) }
}

Architecture

24. What is MVVM and how is it implemented in Android?

MVVM (Model-View-ViewModel):

  • Model — data layer (repository, data sources, entities)
  • View — UI (Activity, Fragment, Composable) — observes ViewModel
  • ViewModel — mediates between View and Model, holds UI state
View → observes → ViewModel → calls → Repository
                              ↑
                        Model (entities)
// Model
data class Product(val id: Int, val name: String, val price: Double)

// Repository (Model layer)
class ProductRepository(private val api: ProductApi, private val db: ProductDao) {
    fun getProducts(): Flow<List<Product>> = db.getAll()
        .onStart { api.fetchProducts().also { db.insertAll(it) } }
}

// ViewModel
class ProductViewModel(private val repo: ProductRepository) : ViewModel() {
    val products: StateFlow<List<Product>> = repo.getProducts()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
}

// View (Compose)
@Composable
fun ProductScreen(viewModel: ProductViewModel = viewModel()) {
    val products by viewModel.products.collectAsStateWithLifecycle()
    LazyColumn { items(products) { ProductCard(it) } }
}

25. What is Clean Architecture in Android?

Clean Architecture separates code into three layers with strict dependency rules:

Presentation Layer (UI, ViewModel)
       ↓ depends on
Domain Layer (Use Cases, Repository interfaces, Entities)
       ↓ depends on
Data Layer (Repository implementations, Data Sources, DTOs)
// Domain entity
data class User(val id: Int, val name: String)

// Repository interface (domain layer)
interface UserRepository {
    suspend fun getUser(id: Int): User
}

// Use case (domain layer)
class GetUserUseCase(private val repo: UserRepository) {
    suspend operator fun invoke(id: Int): User = repo.getUser(id)
}

// Repository implementation (data layer)
class UserRepositoryImpl(
    private val api: UserApi,
    private val dao: UserDao
) : UserRepository {
    override suspend fun getUser(id: Int): User =
        dao.getUser(id)?.toDomain() ?: api.getUser(id).also { dao.insert(it.toEntity()) }.toDomain()
}

// ViewModel (presentation layer)
class UserViewModel(private val getUser: GetUserUseCase) : ViewModel() {
    fun load(id: Int) {
        viewModelScope.launch {
            _state.value = UiState.Success(getUser(id))
        }
    }
}

Jetpack libraries

26. What is Room and how do you use it?

Room is an ORM (Object Relational Mapper) built on SQLite.

// Entity
@Entity(tableName = "users")
data class UserEntity(
    @PrimaryKey val id: Int,
    @ColumnInfo(name = "full_name") val name: String,
    val email: String
)

// DAO
@Dao
interface UserDao {
    @Query("SELECT * FROM users") fun getAll(): Flow<List<UserEntity>>
    @Query("SELECT * FROM users WHERE id = :id") suspend fun getById(id: Int): UserEntity?
    @Upsert suspend fun upsert(user: UserEntity)
    @Delete suspend fun delete(user: UserEntity)
}

// Database
@Database(entities = [UserEntity::class], version = 1, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

// Build
val db = Room.databaseBuilder(context, AppDatabase::class.java, "app-db")
    .addMigrations(MIGRATION_1_2)
    .build()

27. How does Navigation Component work?

Navigation Component manages Fragment/Composable navigation with a visual nav graph.

// NavHost (Compose)
@Composable
fun AppNavigation() {
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = "home") {
        composable("home") { HomeScreen(navController) }
        composable(
            route = "detail/{id}",
            arguments = listOf(navArgument("id") { type = NavType.IntType })
        ) { backStackEntry ->
            val id = backStackEntry.arguments?.getInt("id") ?: return@composable
            DetailScreen(id, navController)
        }
    }
}

// Navigate
navController.navigate("detail/42")

// Navigate with result
navController.previousBackStackEntry
    ?.savedStateHandle
    ?.set("result", "value")
navController.popBackStack()

28. What is DataStore and why is it preferred over SharedPreferences?

DataStore is the modern replacement for SharedPreferences — it uses Kotlin Coroutines and is safe for async operations.

Feature SharedPreferences DataStore
API Synchronous + callbacks Coroutines / Flow
Thread safety No Yes
Consistent reads No Yes
Typed keys No Yes (Proto DataStore)
Error handling None Exceptions in Flow
// Preferences DataStore
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore("settings")

val DARK_MODE_KEY = booleanPreferencesKey("dark_mode")

// Write
suspend fun setDarkMode(enabled: Boolean) {
    context.dataStore.edit { prefs ->
        prefs[DARK_MODE_KEY] = enabled
    }
}

// Read (Flow)
val darkMode: Flow<Boolean> = context.dataStore.data
    .catch { if (it is IOException) emit(emptyPreferences()) else throw it }
    .map { it[DARK_MODE_KEY] ?: false }

Performance and debugging

29. What causes ANR (Application Not Responding) and how do you prevent it?

ANR occurs when the main (UI) thread is blocked for more than:

  • 5 seconds for user input (touch, key events)
  • 10 seconds for BroadcastReceiver
// Bad — blocking main thread
fun loadData() {
    val data = File("large.json").readText()   // blocks UI!
    updateUI(data)
}

// Good — background thread via coroutines
fun loadData() {
    viewModelScope.launch {
        val data = withContext(Dispatchers.IO) {
            File("large.json").readText()
        }
        updateUI(data)   // back on Main dispatcher
    }
}

StrictMode helps detect accidental disk/network on main thread:

StrictMode.setThreadPolicy(
    StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()
)

30. What is a memory leak in Android? Give examples.

A memory leak occurs when an object is no longer needed but still referenced, preventing garbage collection.

Common Android memory leaks:

// 1. Static reference to Context/Activity
companion object {
    var instance: MyActivity? = null  // NEVER DO THIS
}

// 2. Non-static inner class holding Activity reference
class MyActivity : AppCompatActivity() {
    inner class MyTask : AsyncTask<...>() { ... }  // holds reference to MyActivity
}

// Fix: use static nested class with WeakReference
class MyTask(activity: WeakReference<MyActivity>) : AsyncTask<...>()

// 3. Anonymous listener not unregistered
override fun onResume() {
    super.onResume()
    sensorManager.registerListener(this, sensor, SENSOR_DELAY_NORMAL)
}
// Fix: unregister in onPause
override fun onPause() {
    super.onPause()
    sensorManager.unregisterListener(this)
}

// 4. ViewModel holding View reference
class MyViewModel : ViewModel() {
    var textView: TextView? = null  // NEVER — causes leak
}

Detection: Use LeakCanary library in debug builds.


31. How do you optimise RecyclerView performance?

// 1. Stable IDs
adapter.setHasStableIds(true)

// 2. DiffUtil — only rebind changed items
class UserDiffCallback : DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(old: User, new: User) = old.id == new.id
    override fun areContentsTheSame(old: User, new: User) = old == new
}

class UserAdapter : ListAdapter<User, UserViewHolder>(UserDiffCallback()) {
    // submitList() calculates diff on background thread
}

// 3. ViewHolder pattern — avoid findViewById in onBindViewHolder
class UserViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    private val nameText: TextView = view.findViewById(R.id.name)
    fun bind(user: User) { nameText.text = user.name }
}

// 4. setItemViewCacheSize for smoother scrolling
recyclerView.setItemViewCacheSize(20)

// 5. Paging 3 for large datasets
val flow = Pager(PagingConfig(pageSize = 20)) { UserPagingSource() }.flow
    .cachedIn(viewModelScope)

32. What is the onSaveInstanceState / onRestoreInstanceState pattern?

Used to save and restore small amounts of UI state across configuration changes and process death.

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.putString("username", currentUsername)
    outState.putInt("scroll_pos", layoutManager.findFirstVisibleItemPosition())
}

override fun onRestoreInstanceState(savedInstanceState: Bundle) {
    super.onRestoreInstanceState(savedInstanceState)
    val username = savedInstanceState.getString("username")
    val scrollPos = savedInstanceState.getInt("scroll_pos", 0)
}

// Alternatively, check in onCreate
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    if (savedInstanceState != null) {
        val username = savedInstanceState.getString("username")
    }
}

Networking and data

33. How do you make network requests in Android?

// Retrofit + OkHttp (most common)
interface UserApi {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: Int): UserResponse

    @POST("users")
    suspend fun createUser(@Body user: CreateUserRequest): UserResponse
}

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .client(
        OkHttpClient.Builder()
            .addInterceptor(HttpLoggingInterceptor().apply { level = BODY })
            .connectTimeout(30, TimeUnit.SECONDS)
            .build()
    )
    .build()

val api = retrofit.create(UserApi::class.java)

// In repository — handle errors
suspend fun getUser(id: Int): Result<User> = runCatching {
    api.getUser(id).toDomain()
}

34. What is Dependency Injection and how does Hilt implement it?

Dependency Injection provides objects their dependencies from outside rather than creating them internally.

// Module — tells Hilt how to create instances
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit = Retrofit.Builder()
        .baseUrl(BuildConfig.API_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    @Provides
    @Singleton
    fun provideUserApi(retrofit: Retrofit): UserApi = retrofit.create(UserApi::class.java)
}

// Repository — injected
@Singleton
class UserRepository @Inject constructor(
    private val api: UserApi,
    private val dao: UserDao
)

// ViewModel — injected
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repo: UserRepository
) : ViewModel()

// Activity — entry point
@AndroidEntryPoint
class MainActivity : AppCompatActivity()

Testing

35. How do you write unit tests for a ViewModel?

class UserViewModelTest {
    @get:Rule val mainDispatcherRule = MainDispatcherRule()  // replaces Main dispatcher

    private val mockRepo: UserRepository = mockk()
    private lateinit var viewModel: UserViewModel

    @Before
    fun setUp() {
        viewModel = UserViewModel(mockRepo)
    }

    @Test
    fun `loadUser emits Success state`() = runTest {
        val user = User(1, "Alice")
        coEvery { mockRepo.getUser(1) } returns user

        viewModel.loadUser(1)

        assertEquals(UiState.Success(user), viewModel.uiState.value)
    }

    @Test
    fun `loadUser emits Error on exception`() = runTest {
        coEvery { mockRepo.getUser(any()) } throws IOException("Network error")

        viewModel.loadUser(1)

        assertTrue(viewModel.uiState.value is UiState.Error)
    }
}

36. How do you test Jetpack Compose UI?

@RunWith(AndroidJUnit4::class)
class UserCardTest {

    @get:Rule val composeTestRule = createComposeRule()

    @Test
    fun userCard_displaysName_andFollowButton() {
        val user = User(1, "Alice Smith", "alice@example.com")
        var followClicked = false

        composeTestRule.setContent {
            UserCard(user = user, onFollow = { followClicked = true })
        }

        // Assert UI
        composeTestRule.onNodeWithText("Alice Smith").assertIsDisplayed()
        composeTestRule.onNodeWithText("Follow").assertIsDisplayed()

        // Interact
        composeTestRule.onNodeWithText("Follow").performClick()
        assertTrue(followClicked)
    }
}

Security

37. What are common Android security best practices?

// 1. Don't log sensitive data
Log.d("Auth", "Password: ${user.password}")  // NEVER

// 2. Use EncryptedSharedPreferences for sensitive data
val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()

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

// 3. Certificate pinning (OkHttp)
val spec = CertificatePinner.Builder()
    .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .build()

OkHttpClient.Builder().certificatePinner(spec).build()

// 4. Don't store secrets in source code
// Use BuildConfig + CI/CD secrets

// 5. android:exported="false" for components not meant to be public

38. What is ProGuard / R8 and when should you use it?

R8 (replaces ProGuard) is a code shrinker, obfuscator, and optimiser that:

  • Removes unused code and resources
  • Renames classes/methods/fields to short names
  • Performs dead code elimination and inlining
// build.gradle
buildTypes {
    release {
        minifyEnabled true
        shrinkResources true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}
# proguard-rules.pro
# Keep data classes used with Gson/Retrofit
-keep class com.example.models.** { *; }
# Keep Parcelable implementations
-keep class * implements android.os.Parcelable { *; }

Always test release builds — R8 can break reflection-based code if rules are missing.


Build system

39. What is Gradle and how does Android use it?

Gradle is Android's build system. Android uses the Kotlin DSL (or Groovy DSL) for configuration.

// build.gradle.kts (app module)
android {
    namespace = "com.example.app"
    compileSdk = 35

    defaultConfig {
        applicationId = "com.example.app"
        minSdk = 26
        targetSdk = 35
        versionCode = 1
        versionName = "1.0"
    }

    buildTypes {
        release {
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }

    buildFeatures { compose = true }
    composeOptions { kotlinCompilerExtensionVersion = "1.5.14" }
}

dependencies {
    implementation(libs.androidx.compose.bom)
    implementation(libs.hilt.android)
    ksp(libs.hilt.compiler)
}

40. What is the difference between minSdk, targetSdk, and compileSdk?

minSdk targetSdk compileSdk
Purpose Minimum Android version the app supports The version the app was designed/tested against The SDK version used to compile the app
Affects Who can install the app Runtime behaviour flags Compile-time API access
Should be As low as your user base requires Latest stable API Always latest

Miscellaneous

41. What is Parcelable vs Serializable?

Serializable Parcelable
Framework Java standard Android-specific
Performance Slow (reflection) Fast (manual)
Implementation Auto (marker interface) Manual or @Parcelize
Usage Simple data transfer Pass objects via Intents/Bundles
// Parcelable with @Parcelize (recommended)
@Parcelize
data class User(val id: Int, val name: String) : Parcelable

// Pass in Intent
intent.putExtra("user", user)
val user: User? = intent.getParcelableExtra("user")

42. What is the difference between apply, also, let, run, and with in Kotlin?

Function Context Returns Use case
apply this receiver Configure object
also it receiver Side effects (logging)
let it lambda result Null checks, transform
run this lambda result Execute and return result
with this lambda result Group calls on object
// apply — configure and return same object
val button = Button(context).apply {
    text = "Click me"
    textSize = 16f
    setOnClickListener { /* ... */ }
}

// let — null safety
user?.let { u ->
    showProfile(u)
}

// also — logging
val list = mutableListOf(1, 2, 3)
    .also { Log.d("Debug", "Created list: $it") }

43. What is lazy in Kotlin and when is it useful in Android?

lazy initialises a property only on first access.

class MyActivity : AppCompatActivity() {

    // Initialized only when first accessed (on main thread by default)
    private val viewModel: MyViewModel by viewModels()

    // Lazy with NONE mode for single-thread access
    private val heavyParser by lazy(LazyThreadSafetyMode.NONE) {
        HeavyXmlParser()
    }

    // Lazily inflated view (useful in ViewHolder)
    private val expandedView: View by lazy {
        LayoutInflater.from(context).inflate(R.layout.expanded, this, false)
    }
}

44. What is the difference between launch and async in Coroutines?

// launch — fire and forget, returns Job
val job = viewModelScope.launch {
    repo.saveUser(user)   // result is not needed
}
job.cancel()  // can be cancelled

// async — returns Deferred<T>, use await() to get result
val deferred = viewModelScope.async {
    repo.getUser(id)
}
val user = deferred.await()   // suspends until result

// Parallel execution with async
viewModelScope.launch {
    val userDeferred = async { repo.getUser(id) }
    val postsDeferred = async { repo.getPosts(id) }
    val user = userDeferred.await()
    val posts = postsDeferred.await()
    // both requests run concurrently
}

45. What is Channel in Kotlin Coroutines?

A Channel is a hot stream for communication between coroutines (like BlockingQueue but suspend-based).

val channel = Channel<Int>(capacity = Channel.BUFFERED)

// Producer
viewModelScope.launch {
    for (i in 1..10) {
        channel.send(i)          // suspends if buffer full
        delay(100)
    }
    channel.close()
}

// Consumer
viewModelScope.launch {
    for (value in channel) {     // loop ends when channel is closed
        println(value)
    }
}

// Prefer Flow for most use cases; use Channel for producer-consumer hot streams

46. How do you handle deep links in Android?

<!-- AndroidManifest.xml -->
<activity android:name=".MainActivity">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" android:host="example.com" android:pathPrefix="/product" />
    </intent-filter>
</activity>
// Handle in Activity
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    intent?.data?.let { uri ->
        val productId = uri.lastPathSegment
        navigateToProduct(productId)
    }
}

// Navigation Component — automatic deep link handling
NavDeepLink { uriPattern = "https://example.com/product/{id}" }

47. What is Jetpack Paging 3?

Paging 3 loads data incrementally from a PagingSource (local DB, network, or both).

// PagingSource
class UserPagingSource(private val api: UserApi) : PagingSource<Int, User>() {
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
        val page = params.key ?: 1
        return try {
            val response = api.getUsers(page, params.loadSize)
            LoadResult.Page(
                data = response.users,
                prevKey = if (page == 1) null else page - 1,
                nextKey = if (response.users.isEmpty()) null else page + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
    override fun getRefreshKey(state: PagingState<Int, User>) = state.anchorPosition
}

// ViewModel
val users: Flow<PagingData<User>> = Pager(PagingConfig(pageSize = 20)) {
    UserPagingSource(api)
}.flow.cachedIn(viewModelScope)

// Compose
val users = viewModel.users.collectAsLazyPagingItems()
LazyColumn {
    items(users, key = { it.id }) { user ->
        if (user != null) UserCard(user)
    }
}

48. What is the difference between Dispatchers.IO, Dispatchers.Default, and Dispatchers.Main?

Dispatcher Thread pool Use case
Dispatchers.Main Main/UI thread UI updates
Dispatchers.IO 64 threads (elastic) File I/O, network, database
Dispatchers.Default CPU cores count CPU-bound computation
Dispatchers.Unconfined Caller thread Rare — testing
viewModelScope.launch {
    val data = withContext(Dispatchers.IO) {
        File("large.json").readText()   // off main thread
    }
    // back on Main
    _uiState.value = UiState.Success(parseJson(data))
}

49. How does ConstraintLayout differ from other layouts?

ConstraintLayout allows creating complex layouts without nested ViewGroups, reducing view hierarchy depth and improving performance.

<androidx.constraintlayout.widget.ConstraintLayout>
    <ImageView
        android:id="@+id/avatar"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />

    <TextView
        android:id="@+id/name"
        app:layout_constraintStart_toEndOf="@id/avatar"
        app:layout_constraintTop_toTopOf="@id/avatar"
        app:layout_constraintEnd_toEndOf="parent" />

    <!-- Guideline — virtual helper at 50% width -->
    <androidx.constraintlayout.widget.Guideline
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>

In Jetpack Compose, use ConstraintLayout (from constraintlayout-compose) only for complex, hard-to-achieve-with-Column/Row layouts.


50. What are common Android anti-patterns to avoid?

Anti-pattern Problem Fix
Context in ViewModel Memory leak when Activity destroyed Use ApplicationContext or none
runBlocking in coroutine Can deadlock, blocks thread Use suspend functions
Network on main thread ANR Move to Dispatchers.IO
Hardcoded strings in code Not translatable Use strings.xml
Large Activities/Fragments Poor separation MVVM + use cases
Ignoring lifecycle Crash on config change ViewModel + repeatOnLifecycle
Static references to Activity Memory leak WeakReference or eliminate
Not using DiffUtil Janky RecyclerView ListAdapter + DiffUtil.ItemCallback

Android vs. cross-platform

Feature Android (Kotlin) React Native Flutter
Language Kotlin JavaScript/TypeScript Dart
UI Native Views + Compose Native Views via bridge Custom engine (Impeller)
Performance Best native Good (JSI bridge) Very good
Code reuse Android-only iOS + Android iOS + Android + Web + Desktop
Ecosystem Google Play, Jetpack npm pub.dev
Hot reload Instant run Fast refresh Hot reload
Best for Native Android apps JS teams, mixed mobile Pixel-perfect cross-platform

6 FAQ

Q: Should I learn Views or Jetpack Compose for new projects?
A: Jetpack Compose for all new projects. Google has declared it the modern toolkit; Compose is the primary focus of new Jetpack features. Learn View system basics for maintaining legacy code.

Q: What is the recommended architecture for Android apps in 2025?
A: Google's official recommendation is Clean Architecture + MVVM: Compose UI → ViewModel (StateFlow) → Use Cases → Repository → Data Sources (Room + Retrofit). Dependency injection via Hilt.

Q: How do I handle configuration changes without losing state?
A: Use ViewModel for in-memory state (survives rotation). For state that must survive process death, use SavedStateHandle in ViewModel or rememberSaveable in Compose.

Q: What is the difference between Fragment and Composable?
A: Fragment is a View-system concept — a reusable UI piece with its own lifecycle. Composable is a Compose concept — a Kotlin function that describes UI. In a full-Compose app, you typically have one Fragment/Activity as host and navigate between Composable screens.

Q: When should I use coroutineScope vs supervisorScope?
A: coroutineScope cancels all children if one fails. supervisorScope lets children fail independently — use it when you want to run parallel tasks and handle each failure separately (e.g., loading user + loading posts — one failure shouldn't cancel the other).

Q: How do I migrate from LiveData to Flow?
A: In ViewModel, replace MutableLiveData<T> with MutableStateFlow<T> and expose as StateFlow. In the UI, replace observe(viewLifecycleOwner) with lifecycleScope.launch { repeatOnLifecycle(STARTED) { flow.collect { ... } } } or use collectAsStateWithLifecycle() in Compose.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools