第十二章:安卓实战架构与最佳实践

本章综合前十一章精华,深入 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 代码检查 ⭐⭐⭐⭐ ⭐⭐⭐
相关推荐
angerdream3 小时前
Android手把手编写儿童手机远程监控App之四大组件详解2
android
kerli3 小时前
基于 kmp/cmp 的跨平台图片加载方案 - 适配 Android View/Compose/ios
android·前端·ios
半条-咸鱼4 小时前
基于安卓的 WAV 音频采集方案_含工具
android·音视频
一个有温度的技术博主4 小时前
Spring Cloud 入门与实战:从架构拆分到核心组件详解
spring·spring cloud·架构
九皇叔叔4 小时前
MySQL8.0 版本安装部署
android·adb
小江的记录本5 小时前
【系统设计】《2026高频经典系统设计题》(秒杀系统、短链接系统、订单系统、支付系统、IM系统、RAG系统设计)(完整版)
java·后端·python·安全·设计模式·架构·系统架构
Mintopia5 小时前
接口为什么越写越难改:从一开始就能避免的设计问题
架构