Irbisa · Шпаргалка
Архитектура Android
MVVM, Clean Architecture, Repository, ViewModel, UseCase, UDF
Что такое паттерн Repository и зачем его используют в Android-приложениях?
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)}…Что такое однонаправленный поток данных (UDF) на Android-экране?
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}…Какие классические паттерны проектирования можно найти внутри Android SDK и популярных библиотек?
Что такое lifecycle-aware компоненты и как узнать, что всё приложение ушло в фон?
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 и MVI на Android: чем они на самом деле различаются и к чему приходит приложение на Compose?
Что такое Clean Architecture на Android: какие в ней слои, что такое правило зависимостей и где живут UseCase?
Как
ViewModelпереживает поворот экрана, когда она его не переживает и как в неё попадают параметры конструктора?class NotesViewModel( private val repo: NoteRepository, private val savedState: SavedStateHandle) : ViewModel() { // survives rotation (the ViewModel itself) and process death (the handle)…Что не так с этой 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() } }}LiveDataилиStateFlowв ViewModel: что даёт каждый из них, и почему новые приложения выбираютStateFlow?// 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…Что получит наблюдатель
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 }}Что должно лежать внутри UseCase и когда добавление UseCase оказывается просто церемонией?
// One operation, one call site, no Android importsclass PlaceOrder @Inject constructor( private val cart: CartRepository, private val orders: OrderRepository, private val pricing: PricingRepository) {…Как моделировать состояние экрана: sealed-иерархией Loading/Success/Error или одним 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}…Как отправить одноразовое событие (навигация, показ snackbar) из ViewModel, и как
SharedFlow,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…Найдите баг: пользователи жалуются, что выбранный на экране поиска фильтр иногда сбрасывается сам.
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) } }}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)…Что делает Android-код трудным для юнит-тестирования и какие решения делают ViewModel и UseCase тестируемыми?
// 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…Расскажите об архитектуре вашего последнего Android-приложения.
Экрану нужны профиль, корзина и фичефлаг, каждый приходит своим Flow из репозитория. Как собрать из них один
StateFlow<UiState>?class CheckoutViewModel @Inject constructor( profiles: ProfileRepository, cart: CartRepository, flags: FeatureFlags) : ViewModel() { val state: StateFlow<CheckoutUiState> = combine(…Найдите баг: экран заказов бесконечно мигает между спиннером и списком, а запрос к базе выполняется снова и снова.
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)}В снекбаре написано «HttpException: HTTP 422», а отменённый запрос после ухода пользователя с экрана показывается как ошибка. Что не так с обработкой ошибок?
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}…Как бы вы разбили большое Android-приложение на Gradle-модули, какой модуль от какого может зависеть и как разорвать цикл между двумя фичами?
// 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…Вам достаётся приложение на фрагментах и XML, где бизнес-логика живёт в фрагментах. Как перевести его на Compose и MVVM без переписывания с нуля?
// 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)…