Irbisa · Шпаргалка

Архитектура Android

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

Middle-разработчик · 22 вопроса

  1. 01

    Что такое паттерн Repository и зачем его используют в Android-приложениях?

    Repository является единственным классом, у которого остальное приложение спрашивает данные определённого вида: он прячет, пришли ли они из сети, Room, DataStore или памяти, и возвращает доменные модели.

    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

    Что такое однонаправленный поток данных (UDF) на Android-экране?

    Состояние течёт вниз от ViewModel к UI, а события идут вверх от UI к ViewModel; UI никогда не меняет состояние сам, он только просит об этом ViewModel.

    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

    Какие классические паттерны проектирования можно найти внутри Android SDK и популярных библиотек?

    Стоит назвать паттерны, которые сидят внутри SDK (Proxy, Chain of Responsibility, Composite, Decorator и Facade), и к каждому класс, с которым вы работали: retrofit.create, интерсепторы OkHttp, ViewGroup, ContextWrapper, Glide.with(...).

  4. 04

    Что такое lifecycle-aware компоненты и как узнать, что всё приложение ушло в фон?

    Lifecycle-aware компонент наблюдает за LifecycleOwner и сам запускается и останавливается, так что Activity больше не нужно дёргать его из onStart и onStop; для приложения целиком наблюдают за 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 и MVI на Android: чем они на самом деле различаются и к чему приходит приложение на Compose?

    Различие в том, как UI узнаёт об изменении: в MVC и MVP изменения проталкиваются во вью вызовами методов, MVVM выставляет наблюдаемое состояние, а MVI сужает его до одного неизменяемого объекта, который меняется только через объявленные действия.

  6. 06

    Что такое Clean Architecture на Android: какие в ней слои, что такое правило зависимостей и где живут UseCase?

    Код делится на слои presentation, domain и data, а зависимости направлены только внутрь: UI зависит от domain, data-слой реализует интерфейсы, объявленные в domain, а сам domain ничего не импортирует из Android, Retrofit или Room.

  7. 07

    Как ViewModel переживает поворот экрана, когда она его не переживает и как в неё попадают параметры конструктора?

    При смене конфигурации Activity пересоздаётся, но её ViewModelStore передаётся новому экземпляру, поэтому возвращается та же ViewModel; очищается она, только когда владелец завершается насовсем, а смерть процесса не переживает ничего из её содержимого.

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

    Что не так с этой ViewModel и почему ViewModel нельзя держать Activity, View или 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()    }  }}

    Она удерживает Activity после поворота, падает, показывая Toast из фонового потока, продолжает работать после ухода с экрана и позволяет UI писать в своё состояние; ViewModel не должна зависеть ни от чего с временем жизни UI, а работу с UI стоит оставить экрану.

  9. 09

    LiveData или StateFlow в ViewModel: что даёт каждый из них, и почему новые приложения выбирают StateFlow?

    Оба хранят текущее значение, на которое подписан UI.

    // 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

    Что получит наблюдатель count, когда bump() выполняется в главном потоке, и чем отличаются setValue и postValue?

    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  }}

    Наблюдатель получит 0, затем 3, затем 2: setValue доставляет значение сразу, а postValue лишь запоминает последнее и выполняет одну задачу в главном потоке позже, поэтому 1 теряется, а устаревшее 2 перезаписывает 3.

  11. 11

    Что должно лежать внутри UseCase и когда добавление UseCase оказывается просто церемонией?

    UseCase описывает одну бизнес-операцию, оформленную как единственный вызываемый класс, и он оправдан, когда логика разделяется между экранами или слишком сложна, чтобы ею владела ViewModel.

    // 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

    Как моделировать состояние экрана: sealed-иерархией Loading/Success/Error или одним data-классом с флагами? Что ломается в каждом варианте?

    Sealed-иерархия не даёт перепутать взаимоисключающие фазы, а один data-класс закрывает куда более частый случай, когда контент, обновление и ошибка истинны одновременно.

    // 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

    Как отправить одноразовое событие (навигация, показ snackbar) из ViewModel, и как SharedFlow, Channel или состояние могут его потерять или повторить?

    SharedFlow теряет событие, отправленное, пока экран ничего не собирает, флаг в состоянии срабатывает повторно после поворота, если UI его не сбросил, а Channel хранит событие, пока его не заберёт один коллектор.

    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

    Найдите баг: пользователи жалуются, что выбранный на экране поиска фильтр иногда сбрасывается сам.

    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() записывает обратно снимок состояния, сделанный до того, как repo.search приостановился, поэтому всё, что изменилось, пока запрос был в пути (например, фильтр, выбранный во время загрузки), затирается старым значением, когда приходят результаты.

  15. 15

    API отдаёт UserDto, Room хранит UserEntity, экран рисует UserUiModel. Когда три класса оправданы, а когда это просто код маппинга?

    Второй класс оправдан только тогда, когда иначе два слоя пришлось бы менять одновременно; любой другой маппер означает сопровождение, за которое вы платите просто так.

    // 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

    Что делает Android-код трудным для юнит-тестирования и какие решения делают ViewModel и UseCase тестируемыми?

    Код становится нетестируемым, когда сам тянется за зависимостями (синглтонами, Context, настоящими часами, захардкоженным диспетчером), а не получает их; внедрение через конструктор плюс несколько швов решают почти всё.

    // 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

    Расскажите об архитектуре вашего последнего Android-приложения.

    Моё последнее приложение написано на Compose и Hilt с модулями :app, :feature:* и :core:*, где ViewModel экрана отдаёт один StateFlow поверх репозитория на Room; затем назовите одно решение, которое защитили бы, и одно, которое изменили бы.

  18. 18

    Экрану нужны профиль, корзина и фичефлаг, каждый приходит своим Flow из репозитория. Как собрать из них один StateFlow<UiState>?

    Источники сводят в один объект состояния, который рисует экран, а результат публикуют через stateIn, чтобы поворот экрана не перезапускал все запросы снизу.

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

    Найдите баг: экран заказов бесконечно мигает между спиннером и списком, а запрос к базе выполняется снова и снова.

    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 объявлен кастомным геттером, поэтому каждое чтение строит новую цепочку stateIn, которая начинается с isLoading = true и заново запускает запрос; экран читает его при каждой рекомпозиции, так что каждый пришедший список возвращает свежий flow в состоянии загрузки.

  20. 20

    В снекбаре написано «HttpException: HTTP 422», а отменённый запрос после ухода пользователя с экрана показывается как ошибка. Что не так с обработкой ошибок?

    Ошибки нужно переводить на той же границе, что и данные: репозиторий превращает транспортные сбои в доменный тип, который UI разбирает исчерпывающе, а то, чем он их ловит, обязано пропускать CancellationException наружу нетронутым.

    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

    Как бы вы разбили большое Android-приложение на Gradle-модули, какой модуль от какого может зависеть и как разорвать цикл между двумя фичами?

    Один тонкий :app, по модулю на фичу и общие модули :core:* под ними; фичи зависят от core и никогда друг от друга, и только :app видит всё.

    // 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

    Вам достаётся приложение на фрагментах и XML, где бизнес-логика живёт в фрагментах. Как перевести его на Compose и MVVM без переписывания с нуля?

    Мигрируют экран за экраном, от листьев внутрь, добавляя сначала ViewModel и только потом Compose, чтобы каждый коммит можно было выкатить.

    // 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)…