Irbisa · Cheatsheet

Android Architecture

MVVM, Clean Architecture, Repository, ViewModel, UseCase, UDF

Middle Developer · 22 questions

  1. 01

    What is the Repository pattern, and why do Android apps use it?

    A Repository is the one class the rest of the app asks for a given kind of data: it hides whether the data came from the network, Room, DataStore or memory, and it returns domain models.

    data class User(val id: Long, val name: String, val avatarUrl: String?) interface UserRepository {  fun observe(id: Long): Flow<User?>  suspend fun refresh(id: Long)}…
  2. 02

    What is unidirectional data flow (UDF) in an Android screen?

    State flows down from the ViewModel to the UI, and events flow up from the UI to the ViewModel; the UI never changes state itself, it only asks the ViewModel to.

    data class CartUiState(val items: List<CartItem> = emptyList(), val isLoading: Boolean = false) sealed interface CartAction {  data class Remove(val id: Long) : CartAction  data object Refresh : CartAction}…
  3. 03

    Which classic design patterns can you point to inside the Android SDK and popular libraries?

    The SDK-internal patterns worth naming are Proxy, Chain of Responsibility, Composite, Decorator and Facade, each tied to a class you have used: retrofit.create, OkHttp interceptors, ViewGroup, ContextWrapper, Glide.with(...).

  4. 04

    What are lifecycle-aware components, and how do you find out that the whole app went to the background?

    A lifecycle-aware component observes a LifecycleOwner and starts or stops itself, so the Activity no longer has to call it from onStart and onStop; for the app as a whole you observe ProcessLifecycleOwner.

    class AppForegroundTracker : DefaultLifecycleObserver {  private val _inForeground = MutableStateFlow(false)  val inForeground: StateFlow<Boolean> = _inForeground.asStateFlow()   override fun onStart(owner: LifecycleOwner) { _inForeground.value = true }  override fun onStop(owner: LifecycleOwner) { _inForeground.value = false }…
  5. 05

    MVC, MVP, MVVM and MVI on Android: what actually differs, and which one does a Compose app end up with?

    They differ in how the UI learns that something changed: MVC and MVP push changes into the view through method calls, MVVM exposes observable state, and MVI narrows that to one immutable state object changed only through declared actions.

  6. 06

    What is Clean Architecture on Android: which layers does it have, what is the dependency rule, and where do UseCases live?

    Code is split into presentation, domain and data layers, and dependencies point only inward: the UI depends on the domain, the data layer implements interfaces the domain declares, and the domain imports nothing from Android, Retrofit or Room.

  7. 07

    How does a ViewModel survive rotation, when does it not survive, and how do constructor parameters get into it?

    On a configuration change the Activity is recreated but its ViewModelStore is handed to the new instance, so the same ViewModel comes back; it is cleared only when the owner finishes for good, and nothing in it survives process death.

    class NotesViewModel(  private val repo: NoteRepository,  private val savedState: SavedStateHandle) : ViewModel() {   // survives rotation (the ViewModel itself) and process death (the handle)…
  8. 08

    What is wrong with this ViewModel, and why must a ViewModel never hold an Activity, View or Context?

    class ProfileViewModel(  private val activity: Activity,  private val repo: ProfileRepository) : ViewModel() {  val state = MutableStateFlow(ProfileUiState())   fun load() {    GlobalScope.launch {      val user = repo.user()      state.value = state.value.copy(name = user.name)      Toast.makeText(activity, activity.getString(R.string.loaded), Toast.LENGTH_SHORT).show()    }  }}

    It leaks the Activity across rotation, crashes when it shows the Toast from a background thread, keeps working after the screen is gone, and lets the UI write its state; a ViewModel should depend on nothing with a UI lifetime and leave UI work to the screen.

  9. 09

    LiveData or StateFlow in a ViewModel: what does each give you, and why do new apps pick StateFlow?

    Both hold a current value the UI observes.

    // LiveData: observe() is lifecycle-aware on its own; map runs on the main threadclass ProfileLiveDataViewModel(repo: ProfileRepository) : ViewModel() {  val name: LiveData<String> = repo.observe().asLiveData().map { it.name }} // StateFlow: an initial value, Flow operators, no Android types in the state…
  10. 10

    What does an observer of count receive when bump() runs on the main thread, and how do setValue and postValue differ?

    class CounterViewModel : ViewModel() {  private val _count = MutableLiveData(0)  val count: LiveData<Int> = _count   // called on the main thread while an observer is STARTED  fun bump() {    _count.postValue(1)    _count.postValue(2)    _count.value = 3  }}

    The observer gets 0, then 3, then 2: setValue delivers at once, while postValue only stores the latest value and runs one main-thread task later, so 1 is lost and the stale 2 overwrites 3.

  11. 11

    What belongs inside a UseCase, and when is adding one just ceremony?

    A UseCase is one business operation expressed as a single callable class, and it earns its place when the logic is shared between screens or too involved for a ViewModel to own.

    // One operation, one call site, no Android importsclass PlaceOrder @Inject constructor(  private val cart: CartRepository,  private val orders: OrderRepository,  private val pricing: PricingRepository) {…
  12. 12

    Do you model a screen's state as a sealed Loading/Success/Error hierarchy or as one data class with flags? What breaks with each?

    A sealed hierarchy makes mutually exclusive phases impossible to confuse, while a single data class handles the far more common case where content, a refresh and an error are all true at the same time.

    // Mutually exclusive phases: a first load with nothing to show yetsealed interface ProfileUiState {  data object Loading : ProfileUiState  data class Error(val message: String, val canRetry: Boolean) : ProfileUiState  data class Ready(val profile: Profile) : ProfileUiState}…
  13. 13

    How do you send one-time events (navigate, show a snackbar) from a ViewModel, and how can a SharedFlow, a Channel or state each lose or repeat them?

    A SharedFlow drops an event emitted while the screen is not collecting, a state flag fires again after rotation unless the UI clears it, and a Channel keeps the event until one collector takes it.

    class CheckoutViewModel(private val orders: OrderRepository) : ViewModel() {  // Channel: buffered while nobody collects, each event reaches one collector  private val _events = Channel<CheckoutEvent>(Channel.BUFFERED)  val events: Flow<CheckoutEvent> = _events.receiveAsFlow()   // State: the message stays until the UI reports it shown…
  14. 14

    Find the bug: users say the filter they pick on the search screen sometimes resets on its own.

    class SearchViewModel(private val repo: SearchRepository) : ViewModel() {  private val _state = MutableStateFlow(SearchUiState())  val state: StateFlow<SearchUiState> = _state.asStateFlow()   fun onFilterChange(filter: Filter) {    _state.update { it.copy(filter = filter) }  }   fun refresh() {    viewModelScope.launch {      val current = _state.value      val results = repo.search(current.query)      _state.value = current.copy(results = results)    }  }}

    refresh() writes back a snapshot of the state taken before repo.search suspended, so anything changed while the request was in flight, such as a filter picked during loading, is overwritten with the old value when the results arrive.

  15. 15

    Your API returns a UserDto, Room stores a UserEntity and the screen renders a UserUiModel. When are three classes right, and when are they just mapping code?

    A second model earns its keep only when two layers would otherwise be forced to change together; every other mapper is maintenance you pay for nothing.

    // data layer: shaped by the wire@Serializabledata class UserDto(  @SerialName("display_name") val displayName: String? = null,  @SerialName("created_at") val createdAt: String? = null)…
  16. 16

    What makes Android code hard to unit-test, and which design choices make ViewModels and UseCases testable?

    Code becomes untestable when it reaches out for its dependencies (singletons, Context, the real clock, a hardcoded dispatcher) instead of receiving them; constructor injection plus a few seams fixes almost all of it.

    // before: reaches out, so a test controls neither the data nor the timeclass TrialBannerViewModel : ViewModel() {  val showBanner = Prefs.getInstance().trialEndsAt > System.currentTimeMillis()} // after: everything arrives through the constructor…
  17. 17

    Describe the architecture of your last Android app.

    My last app was a Compose and Hilt app with :app, :feature:* and :core:* modules, where a screen's ViewModel exposes one StateFlow over a Room-backed repository; then name one decision you would defend and one you would change.

  18. 18

    A screen needs the profile, the cart and a feature flag, each arriving as its own repository Flow. How do you turn those into one StateFlow<UiState>?

    Combine the sources into the single state object the screen renders, then share the result with stateIn so a rotation does not restart every query underneath.

    class CheckoutViewModel @Inject constructor(  profiles: ProfileRepository,  cart: CartRepository,  flags: FeatureFlags) : ViewModel() {  val state: StateFlow<CheckoutUiState> = combine(…
  19. 19

    Find the bug: the orders screen keeps flickering between a spinner and the list, and the database query runs over and over.

    class OrdersViewModel(private val repo: OrdersRepository) : ViewModel() {  val state: StateFlow<OrdersUiState>    get() = repo.observeOrders()      .map { orders -> OrdersUiState(orders = orders) }      .stateIn(        scope = viewModelScope,        started = SharingStarted.WhileSubscribed(5_000),        initialValue = OrdersUiState(isLoading = true)      )} @Composablefun OrdersScreen(vm: OrdersViewModel) {  val state by vm.state.collectAsStateWithLifecycle()  OrdersList(state)}

    state is a custom getter, so every read builds a new stateIn chain that starts at isLoading = true and runs the query again; the screen reads it on every recomposition, so each list that arrives brings back a fresh, loading flow.

  20. 20

    A snackbar reads "HttpException: HTTP 422" and a cancelled request shows up as a failure after the user left the screen. What is wrong with the error handling?

    Errors have to be translated at the same boundary the data is: the repository turns transport failures into a domain type the UI can handle exhaustively, and whatever it uses to catch them must let CancellationException through untouched.

    sealed interface DataError {  data object Offline : DataError  data object Unauthorized : DataError  data class Invalid(val fields: List<String>) : DataError  data class Unexpected(val cause: Throwable) : DataError}…
  21. 21

    How would you split a large Android app into Gradle modules, which module may depend on which, and how do you break a cycle between two features?

    One thin :app, one module per feature and shared :core:* modules below them; features depend on core and never on each other, and only :app sees everything.

    // settings.gradle.ktsinclude(":app")include(":core:domain", ":core:data", ":core:designsystem")include(":feature:cart", ":feature:profile") // feature/cart/build.gradle.kts: never depends on :feature:profile…
  22. 22

    You inherit a Fragment-and-XML app with the business logic sitting in the Fragments. How do you get it onto Compose and MVVM without a rewrite?

    Migrate screen by screen from the leaves inwards, adding a ViewModel first and Compose second, so that every commit is shippable.

    // Step 2: logic leaves the Fragment, the XML stays exactly where it isclass OrdersFragment : Fragment(R.layout.fragment_orders) {  private val vm: OrdersViewModel by viewModels()   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {    val binding = FragmentOrdersBinding.bind(view)…