Android with Kotlin
What is a ViewModel and why does it survive configuration changes?
A ViewModel holds and prepares UI state for an Activity, Fragment or Compose screen. It is stored in a ViewModelStore that is retained across configuration changes (rotation, dark mode, language change), so when the Activity is recreated the new instance gets the same ViewModel and ongoing work is not restarted. It is cleared (onCleared(), viewModelScope cancelled) only when the screen is finished for good. Never hold a reference to an Activity, View or Context in it (memory leak); use AndroidViewModel or inject an application context if needed.
Show a typical ViewModel that exposes state with StateFlow.
@HiltViewModel
class ProductViewModel @Inject constructor(
private val repo: ProductRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow() // read-only for the UI
init { load() }
fun load() {
viewModelScope.launch {
_uiState.value = UiState.Loading
_uiState.value = try {
UiState.Success(repo.getProducts())
} catch (e: IOException) {
UiState.Error("Check your internet connection")
}
}
}
}Private mutable state, public read-only state, work launched in viewModelScope, and a sealed UiState: this is the MVVM / unidirectional data flow pattern interviewers expect.
Explain the Activity lifecycle and what happens on rotation.
Callbacks: onCreate → onStart (visible) → onResume (interactive) → onPause → onStop (not visible) → onDestroy. On rotation (a configuration change) the Activity is destroyed and recreated: onPause → onStop → onDestroy → onCreate → onStart → onResume. The ViewModel survives; local variables and non-saved Compose remember state do not.
How do you keep state across configuration changes and process death?
| Mechanism | Survives rotation | Survives process death |
|---|---|---|
remember { } in Compose | No | No |
rememberSaveable { } | Yes | Yes (small, Bundle-able data) |
ViewModel fields | Yes | No |
SavedStateHandle in a ViewModel | Yes | Yes (small data like ids, query text) |
| Room / DataStore | Yes | Yes (persistent data) |
How do you collect a Flow safely in the UI?
// Jetpack Compose
@Composable
fun ProductScreen(viewModel: ProductViewModel = hiltViewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
ProductContent(state)
}
// Views (Activity / Fragment)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { render(it) }
}
}Both stop collecting when the app goes to the background (below STARTED) and restart when it returns, saving battery and avoiding UI updates on a stopped screen. A plain lifecycleScope.launch { flow.collect {} } keeps collecting in the background.
How does state work in Jetpack Compose? What is state hoisting?
Compose redraws (recomposes) a composable when a State it reads changes. mutableStateOf creates observable state, remember keeps it across recompositions. State hoisting moves state up to the caller and passes down the value plus an event callback, making the composable stateless, reusable and testable.
@Composable
fun QuantitySelector(quantity: Int, onQuantityChange: (Int) -> Unit) { // stateless
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = { onQuantityChange(quantity - 1) }, enabled = quantity > 1) {
Icon(Icons.Default.Remove, contentDescription = "Decrease")
}
Text("$quantity", style = MaterialTheme.typography.titleMedium)
IconButton(onClick = { onQuantityChange(quantity + 1) }) {
Icon(Icons.Default.Add, contentDescription = "Increase")
}
}
}
@Composable
fun CartItemRow() {
var quantity by rememberSaveable { mutableIntStateOf(1) } // state owner
QuantitySelector(quantity, onQuantityChange = { quantity = it })
}What are side effects in Compose: LaunchedEffect, rememberCoroutineScope, DisposableEffect?
LaunchedEffect(key): runs a suspend block when the composable enters composition, and cancels/restarts it whenkeychanges. Use for one-off loads or collecting events.rememberCoroutineScope(): a scope tied to the composable, used to launch coroutines from callbacks likeonClick(e.g. show a snackbar, scroll a list).DisposableEffect(key): for non-suspend setup that needs cleanup inonDispose(register/unregister a listener or lifecycle observer).- Composables must not do side effects directly in their body, because they can run many times in any order.
How do you use Room with coroutines and Flow?
@Entity(tableName = "products")
data class ProductEntity(
@PrimaryKey val id: Long,
val name: String,
val price: Double,
)
@Dao
interface ProductDao {
@Query("SELECT * FROM products ORDER BY name")
fun observeAll(): Flow<List<ProductEntity>> // re-emits on every table change
@Upsert
suspend fun upsertAll(products: List<ProductEntity>)
@Query("DELETE FROM products WHERE id = :id")
suspend fun deleteById(id: Long)
}suspend DAO functions run off the main thread automatically. Returning Flow makes the query observable, so the UI updates whenever the table changes. This is the base of an offline-first repository: the UI observes Room, and the network only refreshes Room.
How do you call an API with Retrofit and coroutines?
interface ProductApi {
@GET("products")
suspend fun getProducts(@Query("page") page: Int): List<ProductDto>
@POST("orders")
suspend fun placeOrder(@Body order: OrderRequest): Response<OrderDto>
}
class ProductRepository @Inject constructor(
private val api: ProductApi,
private val dao: ProductDao,
) {
val products: Flow<List<ProductEntity>> = dao.observeAll()
suspend fun refresh() {
val remote = api.getProducts(page = 1) // Retrofit is main-safe for suspend calls
dao.upsertAll(remote.map { it.toEntity() })
}
}Retrofit supports suspend functions natively: it returns the body or throws HttpException (non-2xx) / IOException (network). Return Response<T> when you need status codes and headers. A JSON converter (kotlinx.serialization or Moshi) maps DTOs.