Irbisa · Cheatsheet
Android Architecture
MVVM, Clean Architecture, Repository, ViewModel, UseCase, UDF
What is the Repository pattern, and why do Android apps use it?
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)}…What is unidirectional data flow (UDF) in an Android screen?
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}…Which classic design patterns can you point to inside the Android SDK and popular libraries?
What are lifecycle-aware components, and how do you find out that the whole app went to the background?
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 }…MVC, MVP, MVVM and MVI on Android: what actually differs, and which one does a Compose app end up with?
What is Clean Architecture on Android: which layers does it have, what is the dependency rule, and where do UseCases live?
How does a
ViewModelsurvive rotation, when does it not survive, and how do constructor parameters get into it?class NotesViewModel( private val repo: NoteRepository, private val savedState: SavedStateHandle) : ViewModel() { // survives rotation (the ViewModel itself) and process death (the handle)…What is wrong with this ViewModel, and why must a ViewModel never hold an
Activity,VieworContext?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() } }}LiveDataorStateFlowin a ViewModel: what does each give you, and why do new apps pickStateFlow?// 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…What does an observer of
countreceive whenbump()runs on the main thread, and how dosetValueandpostValuediffer?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 }}What belongs inside a UseCase, and when is adding one just ceremony?
// One operation, one call site, no Android importsclass PlaceOrder @Inject constructor( private val cart: CartRepository, private val orders: OrderRepository, private val pricing: PricingRepository) {…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?
// 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}…How do you send one-time events (navigate, show a snackbar) from a ViewModel, and how can a
SharedFlow, aChannelor state each lose or repeat them?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…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) } }}Your API returns a
UserDto, Room stores aUserEntityand the screen renders aUserUiModel. When are three classes right, and when are they just mapping code?// data layer: shaped by the wire@Serializabledata class UserDto( @SerialName("display_name") val displayName: String? = null, @SerialName("created_at") val createdAt: String? = null)…What makes Android code hard to unit-test, and which design choices make ViewModels and UseCases testable?
// 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…Describe the architecture of your last Android app.
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>?class CheckoutViewModel @Inject constructor( profiles: ProfileRepository, cart: CartRepository, flags: FeatureFlags) : ViewModel() { val state: StateFlow<CheckoutUiState> = combine(…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)}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?
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}…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?
// 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…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?
// 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)…