本章综合前十一章精华,深入 Clean Architecture + MVI 架构落地、电商 App 完整项目实践、代码质量守护(KtLint / Detekt / Danger)、响应式设计、无障碍,以及 Android 开发常见面试题汇总。
📋 章节目录
| 节 | 主题 |
|---|---|
| 12.1 | Clean Architecture + MVI 落地实践 |
| 12.2 | 完整项目结构(电商 App) |
| 12.3 | 数据层最佳实践(Repository 模式 + 映射层) |
| 12.4 | 代码质量守护(KtLint / Detekt) |
| 12.5 | 响应式布局与大屏适配 |
| 12.6 | 无障碍(Accessibility) |
| 12.7 | 常见 Android 面试题精解 |
12.1 Clean Architecture + MVI 落地实践
架构分层
┌────────────────────────────────────────────────┐
│ Presentation │
│ Screen → ViewModel → UiState / UiEvent │
├────────────────────────────────────────────────┤
│ Domain │
│ UseCase → Repository Interface → Entity │
├────────────────────────────────────────────────┤
│ Data │
│ Repository Impl → Remote / Local DataSource │
└────────────────────────────────────────────────┘
依赖规则:外层依赖内层(Domain 不依赖 Data)
数据流:UI → ViewModel → UseCase → Repository → DataSource
状态流:DataSource → Repository → UseCase → ViewModel → UI
MVI(Model-View-Intent)模式
kotlin
// MVI 核心三要素
// 1. UiState:UI 的完整状态(不可变)
data class ProductListUiState(
val isLoading: Boolean = false,
val products: List<Product> = emptyList(),
val errorMessage: String? = null,
val searchQuery: String = "",
val selectedCategory: ProductCategory? = null,
val isRefreshing: Boolean = false
) {
val isEmpty: Boolean get() = !isLoading && products.isEmpty() && errorMessage == null
val hasError: Boolean get() = errorMessage != null
}
// 2. UiIntent(用户意图):驱动状态变化的事件
sealed interface ProductListIntent {
object LoadProducts : ProductListIntent
object RefreshProducts : ProductListIntent
data class SearchProducts(val query: String) : ProductListIntent
data class FilterByCategory(val category: ProductCategory?) : ProductListIntent
data class ToggleFavorite(val product: Product) : ProductListIntent
object ClearError : ProductListIntent
}
// 3. UiEffect(单次副作用):不需要保存在状态中的事件
sealed interface ProductListEffect {
data class ShowToast(val message: String) : ProductListEffect
data class NavigateToDetail(val productId: Int) : ProductListEffect
object ScrollToTop : ProductListEffect
}
// ViewModel:MVI 核心协调者
@HiltViewModel
class ProductListViewModel @Inject constructor(
private val getProductsUseCase: GetProductsUseCase,
private val toggleFavoriteUseCase: ToggleFavoriteUseCase,
private val searchProductsUseCase: SearchProductsUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow(ProductListUiState())
val uiState: StateFlow<ProductListUiState> = _uiState.asStateFlow()
private val _effects = MutableSharedFlow<ProductListEffect>(extraBufferCapacity = 16)
val effects: SharedFlow<ProductListEffect> = _effects.asSharedFlow()
// Intent 处理器(单一入口)
fun handleIntent(intent: ProductListIntent) {
when (intent) {
is ProductListIntent.LoadProducts -> loadProducts()
is ProductListIntent.RefreshProducts -> refreshProducts()
is ProductListIntent.SearchProducts -> searchProducts(intent.query)
is ProductListIntent.FilterByCategory -> filterByCategory(intent.category)
is ProductListIntent.ToggleFavorite -> toggleFavorite(intent.product)
is ProductListIntent.ClearError -> clearError()
}
}
private fun loadProducts() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
getProductsUseCase(
category = _uiState.value.selectedCategory,
query = _uiState.value.searchQuery
).collect { result ->
result.fold(
onSuccess = { products ->
_uiState.update {
it.copy(isLoading = false, products = products)
}
},
onFailure = { error ->
_uiState.update {
it.copy(isLoading = false, errorMessage = error.message)
}
}
)
}
}
}
private fun refreshProducts() {
viewModelScope.launch {
_uiState.update { it.copy(isRefreshing = true) }
// 刷新逻辑
_uiState.update { it.copy(isRefreshing = false) }
_effects.emit(ProductListEffect.ScrollToTop) // 刷新后滚动到顶部
}
}
private fun searchProducts(query: String) {
_uiState.update { it.copy(searchQuery = query) }
loadProducts() // 携带新查询重新搜索
}
private fun filterByCategory(category: ProductCategory?) {
_uiState.update { it.copy(selectedCategory = category) }
loadProducts()
}
private fun toggleFavorite(product: Product) {
viewModelScope.launch {
toggleFavoriteUseCase(product).fold(
onSuccess = {
_effects.emit(
ProductListEffect.ShowToast(
if (product.isFavorite) "已取消收藏" else "已加入收藏"
)
)
},
onFailure = { _effects.emit(ProductListEffect.ShowToast("操作失败")) }
)
}
}
private fun clearError() {
_uiState.update { it.copy(errorMessage = null) }
}
init {
loadProducts()
}
}
// Screen 层:连接 ViewModel 与 UI
@Composable
fun ProductListRoute(
onNavigateToDetail: (Int) -> Unit,
viewModel: ProductListViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
// 收集单次副作用
LaunchedEffect(Unit) {
viewModel.effects.collect { effect ->
when (effect) {
is ProductListEffect.ShowToast -> {
Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
}
is ProductListEffect.NavigateToDetail -> {
onNavigateToDetail(effect.productId)
}
is ProductListEffect.ScrollToTop -> {
// 通知列表滚动到顶部
}
}
}
}
ProductListScreen(
uiState = uiState,
onIntent = viewModel::handleIntent,
onNavigateToDetail = {
viewModel.handleIntent(ProductListIntent.LoadProducts)
onNavigateToDetail(it)
}
)
}
// 无状态 UI 组件
@Composable
fun ProductListScreen(
uiState: ProductListUiState,
onIntent: (ProductListIntent) -> Unit,
onNavigateToDetail: (Int) -> Unit
) {
Scaffold(
topBar = {
ProductListTopBar(
searchQuery = uiState.searchQuery,
onSearchChange = { onIntent(ProductListIntent.SearchProducts(it)) }
)
}
) { padding ->
Box(modifier = Modifier.padding(padding).fillMaxSize()) {
when {
uiState.isLoading -> FullScreenLoading()
uiState.hasError -> ErrorContent(
message = uiState.errorMessage!!,
onRetry = { onIntent(ProductListIntent.LoadProducts) },
onDismiss = { onIntent(ProductListIntent.ClearError) }
)
uiState.isEmpty -> EmptyContent(
message = "暂无产品",
onRefresh = { onIntent(ProductListIntent.RefreshProducts) }
)
else -> ProductContent(
products = uiState.products,
isRefreshing = uiState.isRefreshing,
onProductClick = onNavigateToDetail,
onFavoriteClick = { product -> onIntent(ProductListIntent.ToggleFavorite(product)) },
onRefresh = { onIntent(ProductListIntent.RefreshProducts) }
)
}
}
}
}
12.2 完整项目结构
ShopApp/
├── app/
│ ├── src/main/
│ │ ├── AndroidManifest.xml
│ │ └── java/com/example/shopapp/
│ │ ├── ShopApplication.kt
│ │ └── MainActivity.kt
│ └── build.gradle.kts
│
├── feature/
│ ├── home/ ← :feature:home
│ │ └── src/main/java/.../home/
│ │ ├── HomeScreen.kt
│ │ ├── HomeViewModel.kt
│ │ └── HomeNavigation.kt
│ ├── product/ ← :feature:product
│ │ └── src/main/java/.../product/
│ │ ├── list/
│ │ │ ├── ProductListScreen.kt
│ │ │ └── ProductListViewModel.kt
│ │ └── detail/
│ │ ├── ProductDetailScreen.kt
│ │ └── ProductDetailViewModel.kt
│ ├── cart/ ← :feature:cart
│ ├── order/ ← :feature:order
│ └── auth/ ← :feature:auth
│
├── core/
│ ├── common/ ← :core:common(工具、扩展)
│ │ ├── extensions/
│ │ ├── utils/
│ │ └── result/
│ ├── ui/ ← :core:ui(共享组件)
│ │ ├── components/ ← 通用 Composable
│ │ ├── theme/ ← 主题定义
│ │ └── icons/
│ ├── network/ ← :core:network(网络层)
│ │ ├── di/
│ │ └── interceptors/
│ ├── database/ ← :core:database(Room)
│ │ ├── di/
│ │ ├── dao/
│ │ └── entities/
│ ├── datastore/ ← :core:datastore
│ └── navigation/ ← :core:navigation(路由定义)
│
└── domain/
├── product/ ← :domain:product
│ ├── model/
│ │ └── Product.kt
│ ├── repository/
│ │ └── ProductRepository.kt(接口)
│ └── usecase/
│ ├── GetProductsUseCase.kt
│ └── ToggleFavoriteUseCase.kt
└── user/ ← :domain:user
12.3 数据层最佳实践
映射层设计
kotlin
// 三层模型:DTO → Entity → Domain
// DTO(Data Transfer Object):网络层数据模型(与 API 字段一一对应)
data class ProductDto(
@SerializedName("id") val id: Int,
@SerializedName("name") val name: String,
@SerializedName("price") val price: Double,
@SerializedName("img_url") val imageUrl: String,
@SerializedName("category_id") val categoryId: Int,
@SerializedName("description") val description: String?,
@SerializedName("stock") val stock: Int,
@SerializedName("is_fav") val isFavorite: Boolean
)
// Entity(数据库层数据模型)
@Entity(tableName = "products")
data class ProductEntity(
@PrimaryKey val id: Int,
val name: String,
val price: Double,
val imageUrl: String,
val categoryId: Int,
val description: String,
val stock: Int,
val isFavorite: Boolean,
val lastUpdated: Long = System.currentTimeMillis()
)
// Domain(业务层数据模型:只包含 UI 需要的)
data class Product(
val id: Int,
val name: String,
val price: Double,
val imageUrl: String,
val category: ProductCategory,
val description: String,
val isInStock: Boolean,
val isFavorite: Boolean
)
// Mapper 扩展函数
fun ProductDto.toEntity(): ProductEntity = ProductEntity(
id = id,
name = name,
price = price,
imageUrl = imageUrl,
categoryId = categoryId,
description = description ?: "",
stock = stock,
isFavorite = isFavorite
)
fun ProductEntity.toDomain(): Product = Product(
id = id,
name = name,
price = price,
imageUrl = imageUrl,
category = ProductCategory.fromId(categoryId),
description = description,
isInStock = true, // 从其他逻辑派生
isFavorite = isFavorite
)
fun ProductDto.toDomain(): Product = toEntity().toDomain()
// UseCase 设计
// ✅ 一个 UseCase 只负责一个业务规则
class GetProductsUseCase @Inject constructor(
private val productRepository: ProductRepository
) {
operator fun invoke(
category: ProductCategory? = null,
query: String = ""
): Flow<Result<List<Product>>> {
return productRepository.observeProducts()
.map { products ->
var filtered = products
if (category != null) filtered = filtered.filter { it.category == category }
if (query.isNotBlank()) filtered = filtered.filter { it.name.contains(query, true) }
Result.success(filtered)
}
.catch { emit(Result.failure(it)) }
}
}
class ToggleFavoriteUseCase @Inject constructor(
private val productRepository: ProductRepository
) {
suspend operator fun invoke(product: Product): Result<Unit> {
return productRepository.updateFavorite(product.id, !product.isFavorite)
}
}
// Repository 接口(在 Domain 层定义)
interface ProductRepository {
fun observeProducts(): Flow<List<Product>>
suspend fun refreshProducts(): Result<Unit>
suspend fun updateFavorite(productId: Int, isFavorite: Boolean): Result<Unit>
suspend fun getProductById(id: Int): Result<Product>
}
12.4 代码质量守护
KtLint 配置
kotlin
// build.gradle.kts(根项目)
plugins {
id("org.jlleitschuh.gradle.ktlint") version "12.1.0"
}
ktlint {
version.set("1.3.1")
android.set(true)
outputColorName.set("RED")
reporters {
reporter(ReporterType.PLAIN)
reporter(ReporterType.HTML)
}
filter {
exclude("**/build/**")
exclude("**/generated/**")
}
}
// 运行检查
// ./gradlew ktlintCheck
// 自动修复
// ./gradlew ktlintFormat
Detekt 静态分析
kotlin
// build.gradle.kts(根项目)
plugins {
id("io.gitlab.arturbosch.detekt") version "1.23.7"
}
detekt {
config.setFrom(files("$rootDir/detekt.yml"))
buildUponDefaultConfig = true
allRules = false
autoCorrect = true
}
dependencies {
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.7")
// Compose 规则
detektPlugins("ru.kode:detekt-rules-compose:1.4.0")
}
yaml
# detekt.yml(自定义规则)
complexity:
LongMethod:
threshold: 60 # 方法最大行数
LongParameterList:
functionThreshold: 6
CyclomaticComplexMethod:
threshold: 15
naming:
VariableNaming:
variablePattern: '[a-z][a-zA-Z0-9]*'
privateVariablePattern: '_[a-z][a-zA-Z0-9]*|[a-z][a-zA-Z0-9]*'
style:
MaxLineLength:
maxLineLength: 120
WildcardImport:
active: true
performance:
SpreadOperator:
active: true
compose:
ComposableNaming:
active: true
MutableStateAutoboxing:
active: true
ViewModelInjection:
active: true
Git Pre-commit Hook
bash
#!/bin/sh
# .git/hooks/pre-commit(自动设置 chmod +x)
echo "正在运行代码检查..."
# KtLint 检查
./gradlew ktlintCheck
KTLINT_STATUS=$?
# Detekt 静态分析
./gradlew detekt
DETEKT_STATUS=$?
if [ $KTLINT_STATUS -ne 0 ] || [ $DETEKT_STATUS -ne 0 ]; then
echo "❌ 代码检查失败!请修复后再提交。"
echo " 运行 ./gradlew ktlintFormat 自动修复格式"
exit 1
fi
echo "✅ 代码检查通过!"
exit 0
12.5 响应式布局与大屏适配
kotlin
// 窗口大小分类(WindowSizeClass)
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val windowSizeClass = calculateWindowSizeClass(this)
ShopAppTheme {
ShopApp(windowSizeClass = windowSizeClass)
}
}
}
}
@Composable
fun ShopApp(windowSizeClass: WindowSizeClass) {
val navigationType = when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> NavigationType.BOTTOM_NAV // 手机竖屏
WindowWidthSizeClass.Medium -> NavigationType.NAVIGATION_RAIL // 平板竖屏
WindowWidthSizeClass.Expanded -> NavigationType.PERMANENT_DRAWER // 平板横屏/折叠屏展开
else -> NavigationType.BOTTOM_NAV
}
val contentType = when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> ContentType.SINGLE_PANE // 单列
else -> ContentType.DUAL_PANE // 双列(大屏)
}
ShopAppContent(navigationType = navigationType, contentType = contentType)
}
enum class NavigationType { BOTTOM_NAV, NAVIGATION_RAIL, PERMANENT_DRAWER }
enum class ContentType { SINGLE_PANE, DUAL_PANE }
@Composable
fun ShopAppContent(navigationType: NavigationType, contentType: ContentType) {
val navController = rememberNavController()
Row(modifier = Modifier.fillMaxSize()) {
// 侧边导航(大屏)
if (navigationType == NavigationType.NAVIGATION_RAIL) {
AppNavigationRail(navController = navController)
}
if (navigationType == NavigationType.PERMANENT_DRAWER) {
PermanentNavigationDrawer(
drawerContent = { AppDrawerContent(navController = navController) }
) { AppNavHost(navController) }
return
}
// 内容区域
Box(modifier = Modifier.weight(1f)) {
AppNavHost(navController = navController)
}
}
}
// 双列布局(ListDetail)
@Composable
fun ProductListDetailLayout(
products: List<Product>,
selectedProduct: Product?,
onProductSelect: (Product) -> Unit,
contentType: ContentType
) {
if (contentType == ContentType.DUAL_PANE) {
// 大屏:左列表 + 右详情
Row(modifier = Modifier.fillMaxSize()) {
ProductList(
products = products,
onProductClick = onProductSelect,
modifier = Modifier.weight(0.4f)
)
Divider(modifier = Modifier.fillMaxHeight().width(1.dp))
if (selectedProduct != null) {
ProductDetailContent(product = selectedProduct, modifier = Modifier.weight(0.6f))
} else {
EmptyDetailPlaceholder(modifier = Modifier.weight(0.6f))
}
}
} else {
// 手机:只显示列表
ProductList(products = products, onProductClick = onProductSelect)
}
}
@Composable private fun AppNavigationRail(navController: NavController) {}
@Composable private fun AppDrawerContent(navController: NavController) {}
@Composable private fun AppNavHost(navController: NavHostController) {}
@Composable private fun ProductList(products: List<Product>, onProductClick: (Product) -> Unit, modifier: Modifier = Modifier) {}
@Composable private fun ProductDetailContent(product: Product, modifier: Modifier = Modifier) {}
@Composable private fun EmptyDetailPlaceholder(modifier: Modifier = Modifier) {}
12.6 无障碍(Accessibility)
kotlin
// Compose 无障碍适配
@Composable
fun AccessibleProductCard(product: Product, onAddToCart: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.semantics {
// 合并子节点的语义(TalkBack 读整张卡片)
contentDescription = "${product.name},价格 ${product.price} 元" +
if (product.isFavorite) ",已收藏" else ""
// 自定义无障碍角色
role = Role.Button
// 自定义操作
customActions = listOf(
CustomAccessibilityAction("加入购物车") { onAddToCart(); true }
)
},
onClick = { /* 主要点击行为 */ }
) {
Row(modifier = Modifier.padding(16.dp)) {
// 装饰性图片:不需要被 TalkBack 读取
AsyncImage(
model = product.imageUrl,
contentDescription = null, // 装饰性,不读
modifier = Modifier.size(64.dp)
)
Spacer(Modifier.width(16.dp))
Column {
Text(
product.name,
modifier = Modifier.semantics { heading() } // 标记为标题
)
Text(
"¥${product.price}",
modifier = Modifier.clearAndSetSemantics {
// 覆盖默认语义(价格有特殊读法)
contentDescription = "价格:${product.price} 元"
}
)
}
// 收藏按钮:提供明确的内容描述
IconButton(
onClick = { /* 收藏 */ },
modifier = Modifier.semantics {
contentDescription = if (product.isFavorite) "取消收藏" else "收藏"
}
) {
Icon(
imageVector = if (product.isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
contentDescription = null // 父节点已提供描述
)
}
}
}
}
// 动态字体大小支持
@Composable
fun AccessibilityAwareText() {
// ✅ 使用 sp(随系统字体大小缩放)
Text("正文文字", style = TextStyle(fontSize = 16.sp))
// ❌ 不使用 dp 固定字体大小
// Text("正文文字", style = TextStyle(fontSize = 16.dp))
// 动态字体大小限制(防止过大导致布局混乱)
Text(
"标题",
fontSize = with(LocalDensity.current) { 24.sp.coerceAtMost(36.sp) },
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
// 触摸目标大小(最小 48dp)
@Composable
fun MinTouchTargetButton(onClick: () -> Unit) {
Box(
modifier = Modifier
.defaultMinSize(minWidth = 48.dp, minHeight = 48.dp)
.clickable { onClick() },
contentAlignment = Alignment.Center
) {
Icon(Icons.Default.Add, contentDescription = "添加")
}
}
12.7 常见 Android 面试题精解
系统与架构
Q1:Activity 的 launchMode 有哪几种?各有什么区别?
standard(默认)
每次 startActivity 都创建新实例
适用:大多数场景
singleTop
如果栈顶已有该 Activity,不创建新实例,回调 onNewIntent()
适用:通知跳转(防止重复创建)
singleTask
整个任务栈中只有一个实例,如果已存在则弹出其上所有 Activity
回调 onNewIntent(),应用主界面常用此模式
singleInstance
单独在自己的任务栈中,整个系统只有一个实例
适用:极少场景(如浏览器、来电页面)
Q2:Fragment 的 add 与 replace 有什么区别?
kotlin
// add:将 Fragment 叠加在容器上(不销毁原 Fragment)
supportFragmentManager.commit {
add(R.id.container, FragmentB())
addToBackStack("b")
}
// FragmentA 不会调用 onDestroyView(仍在后台,节省重建开销)
// 适用:多标签切换(ViewPager2 使用 add)
// replace:移除容器中所有 Fragment,再添加新 Fragment
supportFragmentManager.commit {
replace(R.id.container, FragmentB())
addToBackStack("b")
}
// FragmentA 调用 onDestroyView → onDestroy(销毁)
// 适用:单容器内容导航
Q3:MVC / MVP / MVVM / MVI 的区别?
MVC(Model-View-Controller)
耦合高,Controller 难测试,Activity/Fragment 即 Controller
MVP(Model-View-Presenter)
解耦 View 与业务,Presenter 可测试
问题:Presenter 持有 View 引用,需处理 View 为空
MVVM(Model-View-ViewModel)
ViewModel 不持有 View 引用,通过 StateFlow/LiveData 通知
问题:状态可能被多处修改,不易追踪状态来源
MVI(Model-View-Intent)
单向数据流:Intent → ViewModel → UiState(不可变)→ View
优点:状态可追溯、可预测、易于测试
Android 中:用 sealed class 定义 Intent,data class 定义 State
Q4:协程的 launch 与 async 有什么区别?
kotlin
// launch:启动不需要返回值的协程(返回 Job)
val job = scope.launch {
doSomething()
}
job.cancel()
// async:启动需要返回值的协程(返回 Deferred<T>)
val deferred = scope.async {
computeValue()
}
val result = deferred.await() // 等待结果
// 并发启动两个任务(async 的典型用法)
val (user, orders) = coroutineScope {
val userDeferred = async { fetchUser() }
val ordersDeferred = async { fetchOrders() }
Pair(userDeferred.await(), ordersDeferred.await())
}
// 两个请求并行执行,而非串行
Q5:ANR 的原因和排查方法?
ANR 触发条件:
主线程 > 5 秒无响应(Input ANR)
BroadcastReceiver > 10 秒未完成 onReceive()
Service > 20 秒未完成 onCreate()/onStartCommand()
常见原因:
1. 主线程执行耗时操作(IO、网络、数据库)
2. 死锁(主线程等待子线程锁)
3. Binder 调用超时(系统服务繁忙)
4. 主线程消息积压(Handler 消息过多)
排查方法:
1. adb shell anr 目录下的 traces.txt
2. Android Studio → App Inspection → CPU Profiler
3. StrictMode:开发时检测主线程 IO/磁盘操作
kotlin
// 开发模式启用 StrictMode
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
)
}
Demo 代码:chapter12
kotlin
// chapter12/ArchitectureDemo.kt
package com.example.androiddemos.chapter12
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
// 完整 MVI Demo(简化版)
data class DemoUiState(
val isLoading: Boolean = false,
val items: List<String> = emptyList(),
val error: String? = null,
val searchQuery: String = ""
) {
val filteredItems = if (searchQuery.isBlank()) items
else items.filter { it.contains(searchQuery, ignoreCase = true) }
}
sealed interface DemoIntent {
object Load : DemoIntent
data class Search(val query: String) : DemoIntent
data class Delete(val item: String) : DemoIntent
object Refresh : DemoIntent
}
@Composable
fun Chapter12ArchitectureDemo() {
var state by remember { mutableStateOf(DemoUiState()) }
val scope = rememberCoroutineScope()
val handleIntent: (DemoIntent) -> Unit = { intent ->
when (intent) {
is DemoIntent.Load, DemoIntent.Refresh -> {
scope.launch {
state = state.copy(isLoading = true, error = null)
delay(1000) // 模拟网络请求
state = state.copy(
isLoading = false,
items = (1..15).map { "产品 $it:¥${it * 99.0}" }
)
}
}
is DemoIntent.Search -> {
state = state.copy(searchQuery = intent.query)
}
is DemoIntent.Delete -> {
state = state.copy(items = state.items - intent.item)
}
}
}
// 首次加载
LaunchedEffect(Unit) { handleIntent(DemoIntent.Load) }
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Text("MVI 架构 Demo", style = MaterialTheme.typography.headlineSmall)
Text("Intent → ViewModel → UiState → UI(单向数据流)",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant)
Spacer(Modifier.height(12.dp))
// 搜索框(Intent 驱动)
OutlinedTextField(
value = state.searchQuery,
onValueChange = { handleIntent(DemoIntent.Search(it)) },
label = { Text("搜索(实时过滤)") },
leadingIcon = { Icon(Icons.Default.Search, null) },
trailingIcon = {
if (state.searchQuery.isNotBlank()) {
IconButton(onClick = { handleIntent(DemoIntent.Search("")) }) {
Icon(Icons.Default.Clear, "清空") }
}
},
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
"${state.filteredItems.size} 件结果",
modifier = Modifier.align(Alignment.CenterVertically),
style = MaterialTheme.typography.bodySmall
)
Spacer(Modifier.weight(1f))
OutlinedButton(
onClick = { handleIntent(DemoIntent.Refresh) },
enabled = !state.isLoading
) {
Text("刷新")
}
}
Spacer(Modifier.height(8.dp))
// 列表内容
when {
state.isLoading -> Box(Modifier.fillMaxWidth().height(200.dp), Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator()
Spacer(Modifier.height(8.dp))
Text("加载中...")
}
}
state.error != null -> Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer),
modifier = Modifier.fillMaxWidth()
) {
Text(state.error!!, modifier = Modifier.padding(16.dp))
}
state.filteredItems.isEmpty() -> Box(Modifier.fillMaxWidth().height(150.dp), Alignment.Center) {
Text("无结果", color = MaterialTheme.colorScheme.onSurfaceVariant)
}
else -> LazyColumn(verticalArrangement = Arrangement.spacedBy(6.dp)) {
items(state.filteredItems.size) { index ->
val item = state.filteredItems[index]
Card(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(item, modifier = Modifier.weight(1f))
IconButton(onClick = { handleIntent(DemoIntent.Delete(item)) }) {
Icon(Icons.Default.Delete, "删除", tint = MaterialTheme.colorScheme.error)
}
}
}
}
}
}
}
}
章节总结
| 知识点 | 必掌握程度 | 面试频率 |
|---|---|---|
| Clean Architecture 分层 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| MVI 模式(State/Intent/Effect) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| DTO → Entity → Domain 映射 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| UseCase 设计原则 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 多模块 feature/core/domain | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 窗口大小分类与响应式布局 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 无障碍语义(contentDescription) | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| launchMode 四种模式 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| ANR 排查方法 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| KtLint / Detekt 代码检查 | ⭐⭐⭐⭐ | ⭐⭐⭐ |