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

本章综合前十一章精华,深入 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 代码检查 ⭐⭐⭐⭐ ⭐⭐⭐
相关推荐
爱勇宝8 小时前
我做了一个只用来搜歌词的小 App
android·前端·后端
众少成多积小致巨11 小时前
JNI (Java Native Interface) 技术手册中文参考指南
android·java·c++
Cerrda14 小时前
开发体验升级:UnoCSS 自定义 SVG 图标热更新方案
架构·前端框架
Coffeeee18 小时前
如何使用Glide和Coil加载WebP动图
android·kotlin·glide
Kstheme18 小时前
把任何 GitHub 仓库变成系统设计课:这个开源项目做到了
架构
Kapaseker18 小时前
5 分钟搞懂 Kotlin DSL
android·kotlin
禅思院18 小时前
路由性能高可用架构实战方案
前端·架构·前端框架
恋猫de小郭19 小时前
AI Agent 开发究竟是啥?如何用 AI 开发 Agent ?深入浅出给你一套概念
android·前端·ai编程
黄林晴19 小时前
Android 17 正式发布!target 37 一大批旧代码直接不能用了
android
Carson带你学Android19 小时前
Android 17 正式发布:AI 终于成了系统能力
android·前端·ai编程