Android架构面试题:MVP/MVVM/MVI都分不清,架构师跟你没关系
1. MVP vs MVVM vs MVI:不是选哪个,是什么场景用哪个?
核心回答
先泼盆冷水:上来就问"你们用什么架构"的面试官,其实自己也没想清楚要面什么。
这三种架构不是什么升级换代的关系,而是解决不同问题的工具:
- MVP :适合小型项目、快速交付。Model和View完全解耦,但Presenter太重,View和Presenter容易形成双向依赖
- MVVM :适合中大型项目、团队协作。Google官方推荐,数据绑定让View和ViewModel单向依赖
- MVI :适合复杂状态、响应式UI。单向数据流,状态不可变,适合状态多且交互复杂的场景
原理/代码
makefile
MVP: View ←→ Presenter ←→ Model
↑ ↓
└────── 接口回调 ───────┘
MVVM: View ←─── 绑定 ──── ViewModel ←─── Repository ←─── Model
(单向依赖)
MVI: Intent → Model → View (单向数据流)
↑
└── 状态不可变,只产生新状态
举个小例子:登录按钮点击
kotlin
// MVP
class LoginPresenter {
fun onLoginClick(username: String, password: String) {
// 直接调用View
view.showLoading()
// ...业务逻辑
view.navigateToHome()
}
}
// MVVM
class LoginViewModel : ViewModel() {
private val _loginState = MutableLiveData<LoginState>()
val loginState: LiveData<LoginState> = _loginState
fun onLoginClick(username: String, password: String) {
viewModelScope.launch {
_loginState.value = LoginState.Loading
val result = repository.login(username, password)
_loginState.value = result.fold(
onSuccess = { LoginState.Success },
onFailure = { LoginState.Error(it.message) }
)
}
}
}
// MVI
sealed class LoginIntent {
data class Login(val username: String, val password: String) : LoginIntent()
}
sealed class LoginState {
object Idle : LoginState()
object Loading : LoginState()
data class Success(val user: User) : LoginState()
data class Error(val message: String) : LoginState()
}
// Intent处理器
fun reduce(intent: LoginIntent): LoginState {
return when(intent) {
is LoginIntent.Login -> LoginState.Loading
// 真正的MVI会在这里做状态转换,而不是直接赋值
}
}
Android实战场景
选MVP:外包项目、短期活动页、demo。你要的是快速出活,不是可维护性。Facebook早期App用的变体MVP。
选MVVM:90%的业务项目。Jetpack全家桶天然适配,LiveData/DataBinding/StateFlow随便选。Google推荐不是没道理的。
选MVI:金融App、聊天App、状态机复杂的场景。微信团队在技术分享中提过类似思路------把用户操作抽象成Intent,状态驱动UI。
面试加分点
面试官想听的不只是"用什么",而是"为什么换"或"为什么选这个"。
加分回答:
- 我们项目从MVP迁到MVVM:Presenter测试困难,View和Presenter通过接口耦合,改一个要改两个文件
- MVVM里ViewModel不是万能的:屏幕旋转等配置变更ViewModel会重建,要配合SavedStateHandle
- MVI的代价:状态类膨胀,每个页面可能几十个状态case。但好处是状态可追溯,出问题能回放
2. MVVM中ViewModel和View的通信方式有哪些?LiveData vs StateFlow vs SharedFlow怎么选?
核心回答
这个问题本质是问:你怎么理解Android的"响应式"?
三种方式各有各的坑,没有银弹:
- LiveData : lifecycle-aware,页面可见才接收数据,但粘性事件问题让人头疼
- StateFlow :协程原生,非粘性,每次collect拿到最新值,但要注意生命周期
- SharedFlow :事件总线,适合一次性事件,但配置复杂
原理/代码
kotlin
// LiveData 粘性事件问题
// 假设Activity重建:
// 1. ViewModel发出 LoginSuccess
// 2. Activity start但还没observe
// 3. Activity resume,observe拿到LoginSuccess(粘性!)
// 4. 用户可能看到闪一下的Loading然后又跳转
// 解决方案1:LiveData配合 SingleLiveEvent(已废弃,不推荐)
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner) { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
}
}
}
// 解决方案2:StateFlow(非粘性)
class LoginViewModel : ViewModel() {
private val _loginState = MutableStateFlow<LoginState>(LoginState.Idle)
val loginState: StateFlow<LoginState> = _loginState.asStateFlow()
// 一次性事件用 SharedFlow
private val _navigationEvent = MutableSharedFlow<NavigationEvent>()
val navigationEvent = _navigationEvent.asSharedFlow()
}
// View侧
lifecycleScope.launch {
viewModel.loginState.collect { state ->
// 非粘性,只有真正collect时才收到
when(state) {
is LoginState.Success -> showSuccess(state.user)
is LoginState.Error -> showError(state.message)
LoginState.Loading -> showLoading()
}
}
}
// SharedFlow订阅(一次性事件)
lifecycleScope.launch {
viewModel.navigationEvent.collect { event ->
when(event) {
is NavigationEvent.ToHome -> navController.navigate(R.id.home)
is NavigationEvent.ToProfile -> navController.navigate(R.id.profile)
}
}
}
Android实战场景
普通数据(UI状态)用 StateFlow:
kotlin
data class UserListState(
val users: List<User> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
)
val state = MutableStateFlow(UserListState())
一次性事件用 SharedFlow:
arduino
private val _toastMessage = MutableSharedFlow<String>()
val toastMessage: SharedFlow<String> = _toastMessage
// 发送
_toastMessage.emit("保存成功")
// 或用 tryEmit(不等待)
_toastMessage.tryEmit("保存成功") // 返回Boolean
为什么SharedFlow适合一次性事件:
- 新订阅者不会收到之前的消息
- 可以配置replay参数决定要不要缓存
- 支持多播(多个View可以同时订阅)
面试加分点
-
说清楚粘性事件的坑:很多新手不知道LiveData默认是粘性的,导致页面重建后收到意外数据
-
知道replay参数的用法:
ini// replay = 0:新订阅者什么都收不到 // replay = 1:新订阅者收到最新一条 MutableSharedFlow<Int>(replay = 1) -
性能考虑:StateFlow比LiveData轻量,因为它不需要处理Lifecycle Owner
3. 一次性事件怎么处理?为什么LiveData不适合做事件?
核心回答
这是面试高频坑。先搞清楚概念:
状态 vs 事件
- 状态:UI当前的样子,比如"正在加载"、"用户列表"------应该持久
- 事件:通知View做一次性的操作,比如"跳转"、"弹Toast"------只执行一次
LiveData的粘性问题:旧值会被保留,新订阅者会立刻收到之前的值。这对状态没问题,但对事件是灾难。
原理/代码
kotlin
// 场景:用户登录成功后跳转Home
// 用户在Home页按返回键回到Login页,然后配置改变导致Activity重建
// 这时新Activity会收到之前的LoginSuccess事件,触发又一次跳转
// 问题复现
class LoginViewModel : ViewModel() {
private val _loginSuccess = MutableLiveData<Boolean>()
val loginSuccess: LiveData<Boolean> = _loginSuccess
}
// 解决1:Event Wrapper(老方案,不推荐但要懂)
class Event<out T>(private val content: T) {
private var hasBeenHandled = false
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) null
else {
hasBeenHandled = true
content
}
}
fun peekContent(): T = content // 如果你非要拿值
}
// 使用
_loginSuccess = Event(true)
// View
viewModel.loginSuccess.observe(this) { event ->
event.getContentIfNotHandled()?.let { success ->
if (success) navigateToHome()
}
}
// 解决2:SharedFlow(推荐)
class LoginViewModel : ViewModel() {
private val _loginSuccess = MutableSharedFlow<Unit>()
val loginSuccess: SharedFlow<Unit> = _loginSuccess
fun login() {
viewModelScope.launch {
_loginSuccess.emit(Unit)
}
}
}
// View
lifecycleScope.launch {
viewModel.loginSuccess.collect {
navigateToHome()
// 不需要Event包装,因为SharedFlow默认非粘性
}
}
// 解决3:Channel(SharedFlow的兄弟)
// Channel是热流,有缓冲但默认容量为0,必须有消费者
private val _loginSuccess = Channel<Unit>(Channel.BUFFERED)
val loginSuccess = _loginSuccess.receiveAsFlow()
Android实战场景
什么时候用SharedFlow:
- 导航事件(跳转、返回)
- Toast/Snackbar消息
- Dialog显示
- 任何"触发一次"的交互
什么时候用StateFlow:
- UI状态(loading、error、data)
- 表单内容
- 任何需要保持的数据
为什么不用Event LiveData了:
- 需要手动处理hasBeenHandled,很容易漏掉
- 多订阅者时处理逻辑复杂
- Google官方推荐StateFlow/SharedFlow,LiveData定位是UI状态持有者
面试加分点
如果你能说出"状态和事件的本质区别"和"粘性事件的原理",面试官会觉得你对Android生命周期理解到位。
加分项:
- 说清楚LiveData为什么是粘性的:设计初衷是为了解决Configuration Change后数据丢失问题
- 理解Channel vs SharedFlow:Channel用于协程间通信,容量为0时是 suspend 的;SharedFlow是多订阅者场景
- 小心repeatOnLifecycle:配合Flow使用时,页面不可见时停止collect,不会收到事件
4. 模块化 vs 组件化:有什么区别?怎么拆?模块间通信怎么做?
核心回答
先纠正一个常见误区:模块化和组件化不是一回事。
表格
| 模块化 | 组件化 | |
|---|---|---|
| 目标 | 代码解耦、职责分离 | 独立编译、独立运行 |
| 粒度 | 按业务/层级划分 | 按功能/UI划分 |
| 结果 | 还是一个App | 可能拆出多个App |
| 典型例子 | feature-user、feature-order | login-component、video-player |
简单说:模块化是代码组织方式,组件化是发布方式。
原理/代码
groovy
scss
// settings.gradle.kts
include(":app")
include(":features:home")
include(":features:profile")
include(":features:order")
include(":modules:network")
include(":modules:common")
include(":components:login") // 这个可以独立运行
// gradle.properties
# 组件化开关
isComponentEnable=true
// app/build.gradle.kts
plugins {
id("com.android.application")
}
android {
namespace = "com.example.app"
defaultConfig {
applicationId = "com.example.app"
}
}
dependencies {
implementation(project(":features:home"))
implementation(project(":features:profile"))
implementation(project(":modules:common"))
// 组件化时排除login,自己独立运行
if (!isComponentEnable) {
implementation(project(":components:login"))
}
}
// components:login/build.gradle.kts
plugins {
id("com.android.application") // 可以运行成独立App
// 或 id("com.android.library") // 作为库
}
模块间通信:Router方案
kotlin
// 1. 定义接口(基于协议而非实现)
interface HomeNavigator {
fun navigateToProfile(userId: String)
fun navigateToOrder(orderId: String)
}
// 2. App模块提供Router实现
class AppRouter : HomeNavigator {
private val navController: NavController
override fun navigateToProfile(userId: String) {
navController.navigate(R.id.profileFragment, bundleOf("userId" to userId))
}
}
// 3. Home模块依赖接口,不依赖实现
class HomeViewModel(
private val navigator: HomeNavigator, // 注入
private val userRepository: UserRepository
) {
fun onUserClick(userId: String) {
// 不直接依赖Profile模块
navigator.navigateToProfile(userId)
}
}
// 4. 手动注入或用Hilt
@Module
@InstallIn(SingletonComponent::class)
object NavigatorModule {
@Provides
fun provideHomeNavigator(router: AppRouter): HomeNavigator = router
}
Android实战场景
按什么维度拆:
- 按业务域:user、order、product、payment
- 按层级:network、database、common、ui-components
- 按团队边界:哪个团队负责哪个模块
拆分的信号:
- 两个模块同时改同一个文件 → 该拆了
- 编译时间超过5分钟 → 该拆了
- 新人上手要改3个模块才能加一个功能 → 该拆了
不要过早拆分:很多小项目强行组件化,维护成本反而更高。MVP/MVVM都没跑清楚之前,别折腾模块化。
面试加分点
-
提到Arouter或AutoRoute:阿里的Arouter是组件化路由的事实标准
-
说出模块化的坑:
- R文件冲突(每个模块有自己R)
- 资源名冲突
- 循环依赖
- 构建速度不见得更快(没有合理拆分的话)
-
了解动态加载:组件化终极形态是App Bundle、插件化。提到"宿主+插件"架构
5. Clean Architecture在Android中怎么落地?UseCase层到底要不要?
核心回答
Clean Architecture是好东西,但Android落地有代价。先说结论:
UseCase不是必须的 ,但分层思想是必须的。
很多项目加了一层UseCase,结果只是:
kotlin
class GetUserUseCase(private val repo: UserRepository) {
suspend operator fun invoke(userId: String): User = repo.getUser(userId)
}
这叫套壳,不叫Clean Architecture。
原理/代码
scss
Clean Architecture 分层(Android版)
┌─────────────────────────────────────┐
│ Presentation │ ViewModel、Activity、Fragment
│ (UI层 / 状态驱动 / 响应式) │
└─────────────────┬───────────────────┘
│
┌─────────────────▼───────────────────┐
│ Domain │ UseCase、Entity、业务规则
│ (纯Kotlin / 无Android依赖 / 可测试)│
└─────────────────┬───────────────────┘
│
┌─────────────────▼───────────────────┐
│ Data │ Repository实现、数据源、网络、DB
│ (外部世界 / 实现细节 / Framework) │
└─────────────────────────────────────┘
UseCase的真正价值 :组合业务逻辑
kotlin
// 场景:下单前要验证用户、验证库存、计算价格、创建订单
// 不用UseCase:全部塞ViewModel
class OrderViewModel {
suspend fun createOrder(productId: String) {
val user = userRepo.getUser()
if (!userRepo.validateUser(user)) throw AuthException()
val stock = inventoryRepo.getStock(productId)
if (stock < 1) throw OutOfStockException()
val price = priceCalculator.calculate(user, productId)
orderRepo.create(...)
}
}
// 用UseCase:每个步骤一个UseCase,可复用、可测试
class ValidateUserUseCase(private val userRepo: UserRepository) {
suspend operator fun invoke(): User {
val user = userRepo.getCurrentUser()
if (!userRepo.isValid(user)) throw AuthException()
return user
}
}
class CheckStockUseCase(private val inventoryRepo: InventoryRepository) {
suspend operator fun invoke(productId: String): Int {
val stock = inventoryRepo.getStock(productId)
if (stock < 1) throw OutOfStockException()
return stock
}
}
class CalculatePriceUseCase(private val priceRepo: PriceRepository) {
suspend operator fun invoke(user: User, productId: String): BigDecimal {
return priceRepo.calculate(user, productId)
}
}
// ViewModel只编排
class OrderViewModel(
private val validateUser: ValidateUserUseCase,
private val checkStock: CheckStockUseCase,
private val calculatePrice: CalculatePriceUseCase,
private val createOrder: CreateOrderUseCase
) {
val orderState = MutableStateFlow<OrderState>(OrderState.Idle)
fun createOrder(productId: String) {
viewModelScope.launch {
orderState.value = OrderState.Loading
try {
val user = validateUser()
checkStock(productId)
val price = calculatePrice(user, productId)
createOrder(productId, price)
orderState.value = OrderState.Success
} catch (e: Exception) {
orderState.value = OrderState.Error(e.message ?: "Unknown")
}
}
}
}
Android实战场景
什么时候需要UseCase:
- 业务逻辑复杂,多个步骤有复用需求
- 需要单元测试,但不想mock太多Repository
- 团队多人协作,需要清晰边界
什么时候不需要:
- 单个Repository调用就能搞定
- 业务简单,ViewModel不臃肿
- 项目小,快速迭代优先
Android落地建议:
bash
domain/
├── model/ # 领域实体
├── repository/ # 仓储接口(抽象,不依赖具体实现)
├── usecase/ # 用例
└── exception/ # 业务异常
data/
├── repository/ # 仓储实现
├── remote/ # 远程数据源
├── local/ # 本地数据源
└── mapper/ # 数据映射
面试加分点
-
说出Domain层为什么是纯Kotlin:不依赖Android才能做单元测试,才能被其他平台复用(Kotlin Multiplatform)
-
理解Dependency Rule:内层不依赖外层,外层依赖内层
-
知道反模式:
- 在UseCase里直接写Android代码
- UseCase只是调用Repository(套壳)
- 跨层依赖(Data直接引用Presentation)
6. Repository模式的正确写法:网络+本地+缓存的策略
核心回答
Repository不是"数据访问层",而是数据来源的决策者。
常见错误:
- 直接暴露Network/DB的接口
- 没有任何缓存策略,每次都请求网络
- 缓存策略混乱,优先级不清楚
正确思路:以用户为中心,给用户最快的数据,同时保证数据新鲜。
原理/代码
kotlin
interface UserRepository {
suspend fun getUser(id: String): Result<User>
suspend fun refreshUser(id: String): Result<User> // 强制刷新
}
// 实现:多层缓存 + 网络 + 本地
class UserRepositoryImpl(
private val network: UserApi,
private val local: UserDao,
private val cache: MemoryCache // LRU cache
) : UserRepository {
// 优先级:内存 → 磁盘 → 网络
// 写策略:网络 → 内存 + 磁盘
override suspend fun getUser(id: String): Result<User> {
// 1. 先查内存缓存(毫秒级)
cache.get(id)?.let { return Result.success(it) }
// 2. 查本地数据库
val localUser = local.getUser(id)
if (localUser != null) {
cache.put(id, localUser) // 回填内存
// 后台刷新,不阻塞
refreshInBackground(id)
return Result.success(localUser)
}
// 3. 请求网络
return fetchFromNetwork(id)
}
override suspend fun refreshUser(id: String): Result<User> {
// 强制刷新:跳过缓存,直接网络
return fetchFromNetwork(id)
}
private suspend fun fetchFromNetwork(id: String): Result<User> {
return try {
val user = network.getUser(id)
// 写入本地和缓存
local.insertUser(user)
cache.put(id, user)
Result.success(user)
} catch (e: Exception) {
// 网络失败时,返回本地数据(如果有)
local.getUser(id)?.let {
Result.success(it)
} ?: Result.failure(e)
}
}
private fun refreshInBackground(id: String) {
// 使用WorkManager或其他机制后台刷新
// 不阻塞主流程
}
}
// Memory Cache 实现
class MemoryCache(private val maxSize: Int = 100) : LinkedHashMap<String, User>(maxSize, 0.75f, true) {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, User>?): Boolean {
return size > maxSize
}
}
Android实战场景
常见的缓存策略:
表格
| 策略 | 适用场景 | 实现难度 |
|---|---|---|
| Cache-Aside | 大多数场景,读多写少 | 低 |
| Read-Through | 需要统一的数据加载入口 | 中 |
| Write-Through | 需要强一致性,写操作频繁 | 中 |
| Write-Behind | 需要高写入性能,可以容忍短暂不一致 | 高 |
Cache-Aside(最常用) :
读:先缓存 → 没有则读数据库 → 没有则读网络
写:更新网络 → 更新数据库
Android特有考虑:
- 网络质量差时,本地缓存是救命稻草
- DiskLruCache/Room 适合大数据缓存
- 内存缓存注意配置变更(Activity重建后丢失)
面试加分点
-
说出Room + Retrofit + Coroutines的配合:Room作为单一数据源,Repository是决策者
-
理解数据新鲜度:聊天列表需要实时,但设置页面可以缓存5分钟
-
知道缓存失效策略:
- TTL(Time To Live)
- 主动失效(推送通知、主动刷新)
- 版本号控制
7. 依赖注入:Hilt vs Koin vs 手动注入,怎么选?
核心回答
先问自己:为什么需要依赖注入?
不是为了"看起来高级",而是为了:
- 解耦:类不直接依赖具体实现
- 可测试:Mock依赖轻松替换
- 生命周期管理:Android有Activity、ViewModel等生命周期
选型建议:
- Hilt:大型项目、团队协作、Google官方支持
- Koin:中小型项目、快速开发、不想用注解
- 手动注入:极小项目、教学目的
原理/代码
kotlin
// 手动注入(最原始,但也是基础)
class UserRepository(private val api: UserApi) { }
// 手动创建对象
class MainActivity : AppCompatActivity() {
private val api = UserApi()
private val repo = UserRepository(api)
private val vm = MainViewModel(repo)
}
// 问题:依赖多了会爆炸,而且难以测试
// Koin(函数式DSL)
val appModule = module {
// 单例
single { UserApi() }
single { UserRepository(get()) }
single { GetUserUseCase(get()) }
// ViewModel要传Activity Context的话
viewModel { MainViewModel(get()) }
}
class MainActivity : AppCompatActivity() {
private val vm: MainViewModel by viewModel()
// 搞定!
}
// Hilt(编译时注解)
@HiltAndroidApp
class MyApp : Application()
@AndroidEntryPoint // 自动生成DI代码
class MainActivity : AppCompatActivity() {
@Inject lateinit var api: UserApi
@Inject lateinit var vm: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 注入已完成
}
}
// ViewModel注入
class MainViewModel @Inject constructor(
private val getUser: GetUserUseCase
) : ViewModel()
// Module定义
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideApi(retrofit: Retrofit): UserApi = retrofit.create()
}
Android实战场景
什么时候选Hilt:
- 项目大,依赖关系复杂
- 需要多模块,每个模块自己提供依赖
- 团队成员多,需要统一规范
- 需要编译时检查,运行时零开销
什么时候选Koin:
- 小团队、个人项目
- 不喜欢注解、喜欢DSL风格
- 快速出MVP
- 项目不复杂,不需要编译时代码生成
什么时候手动:
- 学习DI原理
- 项目极小(就2-3个类)
- 不想引入额外依赖
面试加分点
- 理解编译时 vs 运行时:Hilt在编译时生成代码,运行时无反射开销;Koin在运行时构建图谱
- 说出Hilt的局限性:编译时间长、代码膨胀、灵活性不如Dagger
- 提到其他方案:Dagger2(复杂但最灵活)、manual DI库(如Injekt)
8. Android的状态管理:状态提升、状态下沉、状态容器怎么选?
核心回答
这三种方式对应三种复杂度级别:
表格
| 方式 | 复杂度 | 适用场景 |
|---|---|---|
| State Hoisting(状态提升) | 低 | 单页面、简单组件 |
| State Down(状态下沉) | 中 | 父子组件通信 |
| State Container(状态容器) | 高 | 复杂状态、跨页面 |
记住原则 :状态应该被拥有它所需最少信息的组件持有。
原理/代码
kotlin
// 1. 状态提升(最常用)
// 好:状态共享,兄弟组件能通信
@Composable
fun ParentScreen() {
var text by remember { mutableStateOf("") }
Column {
// 子组件可以读写text
InputField(text, { text = it })
DisplayText(text)
}
}
// 2. 状态下沉(Props Drilling的反模式,应该避免)
// 差:状态在根节点,往下层层传
@Composable
fun App() {
var globalState by remember { mutableStateOf(...) }
// 传5层,每层都要声明这个参数
Level1(state = globalState, onUpdate = { globalState = it })
}
// 3. 状态容器(用ViewModel或State hoisting到合适层级)
// 好:状态归类,逻辑归位
// 场景:购物车页面
data class CartState(
val items: List<CartItem> = emptyList(),
val totalPrice: BigDecimal = BigDecimal.ZERO,
val isCheckoutLoading: Boolean = false,
val checkoutError: String? = null,
val selectedItems: Set<String> = emptySet()
)
class CartViewModel @Inject constructor(
private val cartRepo: CartRepository
) : ViewModel() {
private val _state = MutableStateFlow(CartState())
val state: StateFlow<CartState> = _state.asStateFlow()
fun selectItem(itemId: String) {
_state.update { current ->
val newSelection = if (itemId in current.selectedItems) {
current.selectedItems - itemId
} else {
current.selectedItems + itemId
}
current.copy(selectedItems = newSelection)
}
}
fun checkout() {
viewModelScope.launch {
_state.update { it.copy(isCheckoutLoading = true, checkoutError = null) }
try {
val selectedItems = _state.value.items.filter { it.id in _state.value.selectedItems }
cartRepo.checkout(selectedItems)
_state.update { it.copy(isCheckoutLoading = false) }
} catch (e: Exception) {
_state.update { it.copy(isCheckoutLoading = false, checkoutError = e.message) }
}
}
}
}
Android实战场景
什么时候用哪种:
-
单页面简单状态:用ViewModel + StateFlow
csharp// 比如一个设置开关 var notificationsEnabled by remember { mutableStateOf(true) } -
需要跨组件共享:
kotlin// 两个子组件需要共享同一个状态 @Composable fun SharedStateContainer() { var sharedState by remember { mutableStateOf(...) } ChildA(state = sharedState, onChange = { sharedState = it }) ChildB(state = sharedState, onChange = { sharedState = it }) } -
全局状态:用State Container(App级ViewModel或Hilt Singleton)
kotlin
less// 用户登录状态,全局共享 @Singleton class UserSession @Inject constructor() { private val _isLoggedIn = MutableStateFlow(false) val isLoggedIn: StateFlow<Boolean> = _isLoggedIn.asStateFlow() }
面试加分点
- 说出Jetpack Compose的State Hoisting模式:状态提升到父组件,让子组件更可复用
- 理解一次性事件不提升:Toast/导航用SharedFlow,不放在State里
- 性能考虑 :状态太大导致recompose范围大,用
derivedStateOf优化
9. 多模块项目的Gradle构建优化:怎么加快编译速度?
核心回答
说个扎心的事实:大部分项目的编译慢,不是Gradle的问题,是你代码的问题。
但Gradle配置也有优化空间。
原理/代码
ini
// gradle.properties
# JVM参数
org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# 并行构建
org.gradle.parallel=true
# 配置缓存
org.gradle.configuration-cache=true // Gradle 7.0+
# 按需配置
org.gradle.configure-on-demand=true
# 缓存
org.gradle.caching=true
# 非传递性依赖
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=true
android.nonFinalResIds=true
java
// app/build.gradle.kts
android {
// 增量编译
compileOptions {
isIncremental = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
// 增量编译
freeCompilerArgs += listOf("-Xjsr305=strict")
}
// 构建缓存
buildFeatures {
buildConfig = true
}
}
// 分离模块的依赖配置
dependencies {
// 只在debug时需要的
debugImplementation("com.facebook.stetho:stetho:1.6.0")
// 只在测试时需要的
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
}
less
// 禁用不必要的模块构建
// settings.gradle.kts
include(":features:legacy") // 老代码,不常改
// 在需要的模块里引用
dependencies {
if (project.hasProperty("includeLegacy")) {
implementation(project(":features:legacy"))
}
}
分模块构建:
ruby
# 只构建指定模块
./gradlew :features:home:assembleDebug
# 依赖检查
./gradlew :app:dependencies --configuration debugRuntimeClasspath
Android实战场景
常见的编译慢原因:
- Kotlin增量编译失败:clean后再build就好了,但治标不治本
- databinding/kapt:巨慢,能用ksp就用ksp
- 太多模块:每个模块都是独立项目,初始化开销大
- 依赖图太深 :
apivsimplementation用错
优化清单:
scss
// 1. kapt → ksp(快3-5倍)
plugins {
id("com.google.devtools.ksp")
}
// 2. 减少kapt使用
// Room、Hilt都有ksp支持
dependencies {
ksp("androidx.room:room-compiler:2.6.0")
ksp("com.google.dagger:hilt-compiler:2.48")
}
// 3. 合理使用api/implementation
// implementation:不暴露依赖,传给下游
// api:暴露依赖,下游可以直接用
面试加分点
- 提到Remote Build Cache:团队共享构建缓存
- 说出Gradle版本的重要性:8.x比7.x快很多
- 理解Kotlin Daemon :
kotlin.daemon.jvmargs=-Xmx2g
10. 如何设计一个可测试的Android架构?Unit Test和UI Test怎么写?
核心回答
测试不是"额外工作",是"架构设计正确与否的验证器"。
一个难以测试的架构 ,通常意味着设计有问题。
可测试性原则:
- 依赖抽象,不依赖具体实现
- 副作用(IO、网络)放到边界层
- ViewModel里不要有Android代码
原理/代码
kotlin
// 1. Unit Test:测试ViewModel
class LoginViewModelTest {
private lateinit var viewModel: LoginViewModel
private lateinit var fakeUserRepository: FakeUserRepository
@Before
fun setup() {
fakeUserRepository = FakeUserRepository()
viewModel = LoginViewModel(fakeUserRepository)
}
@Test
fun `login success should emit success state`() = runTest {
// Given
fakeUserRepository.setFakeUser(User("test", "123"))
// When
viewModel.login("test", "123")
// Then
assertEquals(
LoginState.Success,
viewModel.loginState.value
)
}
@Test
fun `login with wrong password should emit error`() = runTest {
// Given
fakeUserRepository.setFakeUser(User("test", "123"))
// When
viewModel.login("test", "wrong")
// Then
val state = viewModel.loginState.value
assertTrue(state is LoginState.Error)
}
}
// Fake Repository实现
class FakeUserRepository : UserRepository {
private var fakeUser: User? = null
fun setFakeUser(user: User) {
fakeUser = user
}
override suspend fun login(username: String, password: String): Result<User> {
return if (fakeUser?.password == password) {
Result.success(fakeUser!!)
} else {
Result.failure(Exception("Invalid credentials"))
}
}
}
less
// 2. UI Test(Espresso)
@HiltAndroidTest
class LoginFragmentTest {
@Inject
lateinit var hiltRule: HiltAndroidRule
@Test
fun `login button click should show loading then navigate`() {
// 准备数据
testDispatcher.testScheduler.advanceTimeBy(1000)
onView(withId(R.id.usernameEditText))
.perform(typeText("testuser"), closeSoftKeyboard())
onView(withId(R.id.passwordEditText))
.perform(typeText("password123"), closeSoftKeyboard())
onView(withId(R.id.loginButton))
.perform(click())
// 验证Loading显示
onView(withId(R.id.progressBar))
.check(matches(isDisplayed()))
// 等待网络请求完成(使用IdlingResource或Fake)
testDispatcher.testScheduler.advanceUntilIdle()
// 验证跳转
intended(hasComponent(HomeActivity::class.java))
}
}
kotlin
// 3. Fake数据层实现
class FakeUserRepositoryImpl @Inject constructor() : UserRepository {
private var shouldFail = false
private var delayMs = 0L
fun setShouldFail(fail: Boolean) {
shouldFail = fail
}
fun setDelay(ms: Long) {
delayMs = ms
}
override suspend fun login(username: String, password: String): Result<User> {
delay(delayMs)
return if (shouldFail) {
Result.failure(Exception("Network error"))
} else {
Result.success(User(username, "token123"))
}
}
}
Android实战场景
测试金字塔:
kotlin
△ UI Test
△ △ △ (少,E2E,慢)
△ △ △ △ △
△ △ △ △ △ △ △ Unit Test
△ △ △ △ △ △ △ △ △ (多,单元,快)
Android测试工具链:
- Unit Test:JUnit4 + MockK/F Mockito + Turbine(Flow测试)
- Instrumented Test:Espresso / Compose UI Test
- Fake:生产环境和测试环境的数据隔离
常见问题:
-
ViewModel有Android依赖:
kotlin
kotlin// 差 class MyViewModel(val context: Context) { } // 好:抽象出来 class MyViewModel(val stringProvider: StringProvider) { } interface StringProvider { ... } -
Repository有真实现:
kotlin
kotlin// 差:依赖Retrofit class UserRepository(private val api: UserApi) // 好:接口隔离 interface UserRepository { suspend fun getUser() } // 测试时用FakeUserRepository
面试加分点
- 说出Test Double的类型:Dummy、Fake、Stub、Mock、Spy,以及什么时候用什么
- 提到Coroutine Test :用
runTest而不是runBlocking,因为后者会阻塞线程 - 理解UI Test的复杂性:UI测试慢、不稳定,但能验证真实用户体验
总结
架构题考的不是你知道几个模式,而是:
- 理解力 :能说清楚每个模式的适用场景和trade-off
- 实战经验:踩过坑,知道什么时候该用什么
- 工程思维:架构是为团队服务的,不是炫技
核心心法:
- 没有银弹,只有取舍
- 先跑通,再优化
- 可测试性是架构好坏的试金石