Android 关于compose的一些坑和理解

** 1.如何在 WindowManager.addView 中使用 Jetpack Compose**

一、引出问题
Android 开发中,很常见的一个场景,通过 WindowManager.addView() 添加一个 View 到屏幕上。Android 最新的视图框架 Jetpack Compose,如何应用进来。这个被添加的 View 如何使用 Compose 编写视图呢?

二、探究问题
有的朋友肯定会马上想到使用 ComposeView 作为桥梁。没错,WindowManager.addView 方法,就接收一个 View 类型的参数。那肯定是要借助 ComposeView 了。但是,经过试验,直接使用 ComposeView 是行不通的。
看代码:

val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
)

val composeView: ComposeView = ComposeView(this).apply {
setContent {
Text(text = “I’m be added”)
}
}

windowManager.addView(composeView, params)
上面代码,编译没有问题,运行时会报错:

FATAL EXCEPTION: main
Process: xxxxxxxx
java.lang.IllegalStateException: ViewTreeLifecycleOwner not found from androidx.compose.ui.platform.ComposeView{8285855 V.E… …I. 0,0-0,0}
at androidx.compose.ui.platform.WindowRecomposer_androidKt.createLifecycleAwareWindowRecomposer(WindowRecomposer.android.kt:352)
at androidx.compose.ui.platform.WindowRecomposer_androidKt.createLifecycleAwareWindowRecomposer d e f a u l t ( W i n d o w R e c o m p o s e r . a n d r o i d . k t : 325 ) a t a n d r o i d x . c o m p o s e . u i . p l a t f o r m . W i n d o w R e c o m p o s e r F a c t o r y default(WindowRecomposer.android.kt:325) at androidx.compose.ui.platform.WindowRecomposerFactory default(WindowRecomposer.android.kt:325)atandroidx.compose.ui.platform.WindowRecomposerFactoryCompanion$LifecycleAware 1. c r e a t e R e c o m p o s e r ( W i n d o w R e c o m p o s e r . a n d r o i d . k t : 168 ) a t a n d r o i d x . c o m p o s e . u i . p l a t f o r m . W i n d o w R e c o m p o s e r P o l i c y . c r e a t e A n d I n s t a l l W i n d o w R e c o m p o s e r 1.createRecomposer(WindowRecomposer.android.kt:168) at androidx.compose.ui.platform.WindowRecomposerPolicy.createAndInstallWindowRecomposer 1.createRecomposer(WindowRecomposer.android.kt:168)atandroidx.compose.ui.platform.WindowRecomposerPolicy.createAndInstallWindowRecomposerui_release(WindowRecomposer.android.kt:224)
at androidx.compose.ui.platform.WindowRecomposer_androidKt.getWindowRecomposer(WindowRecomposer.android.kt:300)
at androidx.compose.ui.platform.AbstractComposeView.resolveParentCompositionContext(ComposeView.android.kt:244)
at androidx.compose.ui.platform.AbstractComposeView.ensureCompositionCreated(ComposeView.android.kt:251)
at androidx.compose.ui.platform.AbstractComposeView.onAttachedToWindow(ComposeView.android.kt:283)
at android.view.View.dispatchAttachedToWindow(View.java:22065)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3553)

看这个错误信息:
应该是从 ComposeView 中没有找到 ViewTreeLifecycleOwner, 其实很好理解。 View 的生命周期依赖于 ViewTreeLifecycleOwner, ComposeView 依赖于一个 ViewCompositonStrategy。核心问题是,ComposeView 需要一个 Lifecycle。

三、解决问题
有了思路自然就尝试解决问题。
首先定义一个 LifecycleOwner ,

import android.view.View
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner

class MyComposeViewLifecycleOwner:
LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {

private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
private val savedStateRegistryController = SavedStateRegistryController.create(this)
private val store = ViewModelStore()override val lifecycle: Lifecycleget() = lifecycleRegistry
override val savedStateRegistry: SavedStateRegistryget() = savedStateRegistryController.savedStateRegistry
override val viewModelStore: ViewModelStoreget() = storefun onCreate() {savedStateRegistryController.performRestore(null)lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}fun onStart() {lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
}fun onResume() {lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}fun onPause() {lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
}fun onStop() {lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
}fun onDestroy() {lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)store.clear()
}/*** Compose uses the Window's decor view to locate the* Lifecycle/ViewModel/SavedStateRegistry owners.* Therefore, we need to set this class as the "owner" for the decor view.*/
fun attachToDecorView(decorView: View?) {decorView?.let {it.setViewTreeViewModelStoreOwner(this)it.setViewTreeLifecycleOwner(this)it.setViewTreeSavedStateRegistryOwner(this)} ?: return
}

}
再看看使用:

private var lifecycleOwner: MyComposeViewLifecycleOwner? = null

val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
)

val composeView: ComposeView = ComposeView(this).apply {
setContent {
Text(text = “I’m be added”)
}
}

// 注意,在 调用 addView 之前:
lifecycleOwner = MyComposeViewLifecycleOwner().also {
it.onCreate() // 注意
it.attachToDecorView(composeView)
}
windowManager.addView(composeView, params)

windowManager.removeViewImmediate(composeView)
lifecycleOwner?.onDestroy()
lifecycleOwner = null
OK,再次运行。成功~

做完这些后,你会发现还有个坑,state 不刷新状态,你需要手动的模拟调用lifecycler生命周期方法。除了oncreate 还要调用onstart onresume,否则不生效。

2.Android compose不是会让代码冗余在一起吗?
Android Compose 的代码冗余问题需要结合其设计理念和使用方式辩证看待。以下从技术原理、实际应用场景和最佳实践角度综合分析:

一、Compose 的设计目标与冗余控制
‌声明式 UI 的简化逻辑‌
Compose 通过声明式编程范式,允许开发者直接描述 UI 的最终状态,无需手动处理视图更新逻辑。传统命令式代码中维护视图状态和同步的冗余操作(如 findViewById 和手动更新视图)被框架自动处理‌47。

‌组件化与复用机制‌
Compose 的 @Composable 函数天然支持模块化拆分,小型 UI 组件(如按钮、卡片)可独立封装并通过参数配置复用,减少重复代码‌16。例如:

kotlin
Copy Code
@Composable
fun CustomButton(text: String, onClick: () -> Unit) {
Button(onClick = onClick) { Text(text) }
}
二、可能导致冗余的场景与解决方案
‌不合理的设计模式‌

‌问题‌:未遵循分层架构(如 MVVM)时,可能将业务逻辑与 UI 代码混杂在 Composable 中,导致代码臃肿‌38。
‌解决方案‌:使用状态管理工具(如 ViewModel + StateFlow)分离逻辑层,保持 Composable 仅关注 UI 渲染‌38。
‌过度组合与嵌套‌

‌问题‌:嵌套过多 Composable 函数可能降低可读性(如深度布局嵌套),但属于代码结构问题而非冗余‌15。
‌解决方案‌:通过 @Stable 注解优化重组性能,或拆分复杂布局为子组件‌8。
三、与传统视图系统的对比
‌对比维度‌ ‌传统 View/XML 方式‌ ‌Jetpack Compose‌
‌代码量‌ 需在 XML 和 Kotlin/Java 间切换,模板代码多 纯 Kotlin 代码,减少 30%-50% 的代码量‌47
‌状态管理‌ 手动同步视图与数据状态(易出错) 自动响应状态变化(声明式驱动)‌47
‌组件复用‌ 依赖自定义 View 或 include 标签 原生函数级组合,参数化配置更灵活‌16
四、最佳实践建议
‌遵循单一职责原则‌
每个 Composable 函数仅实现一个明确功能(如 UserProfileCard 仅处理头像和名称展示)‌38。

‌分层架构强化‌
结合 Clean Architecture 或 MVI 模式,通过 Repository 和 UseCase 隔离数据源与 UI 层‌38。

‌工具链支持‌
利用 Android Studio 的实时预览和重组计数检查(Layout Inspector),快速定位冗余代码‌17。

‌结论‌
Compose 本身通过声明式设计和组件化机制显著降低了代码冗余,但实际开发中是否冗余取决于架构设计和开发者对组合模式的合理运用‌13。遵循分层架构和模块化拆分原则,Compose 可成为高效且简洁的 UI 开发方案。

3.那怎么使用状态管理工具(如 ViewModel + StateFlow)分离逻辑层,保持 Composable 仅关注 UI 渲染‌呢?
好的!我们通过一个简单的 ‌计数器应用‌ 来演示如何用 ViewModel + StateFlow 实现逻辑与 UI 的分离,让 Composable 只负责渲染。

完整示例代码‌
kotlin
Copy Code
// 1️⃣ 数据层/状态定义
data class CounterState(
val count: Int = 0,
val isLoading: Boolean = false
)

// 2️⃣ 逻辑层:ViewModel 处理业务逻辑
class CounterViewModel : ViewModel() {
// 私有状态,通过 StateFlow 对外暴露
private val _state = MutableStateFlow(CounterState())
val state: StateFlow = _state.asStateFlow()

// 事件处理:纯逻辑,不涉及 UI
fun increment() {_state.update { it.copy(count = it.count + 1) }
}fun reset() {_state.update { it.copy(count = 0) }
}

}

// 3️⃣ UI 层:Composable 仅负责显示和事件传递
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val state by viewModel.state.collectAsState()

Column(modifier = Modifier.fillMaxSize(),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally
) {Text(text = "Count: ${state.count}", fontSize = 24.sp)Button(onClick = { viewModel.increment() }) { // ❗️ 事件传递给 ViewModelText("Add +1")}Button(onClick = { viewModel.reset() }) {Text("Reset")}
}

}

关键解析‌

  1. ‌数据层(CounterState)‌
    定义所有 UI 需要显示的状态(如 count 和 isLoading)。
    作用‌:集中管理 UI 状态,避免分散在 Composable 中。

  2. ‌逻辑层(CounterViewModel)‌
    职责‌:处理业务逻辑(如计算、网络请求等)。
    StateFlow‌:通过 StateFlow 暴露状态,确保 UI 能监听状态变化。
    事件处理‌:increment() 和 reset() 方法只修改状态,不涉及 UI 操作。

  3. ‌UI 层(CounterScreen)‌
    仅做三件事‌:
    收集状态‌:通过 collectAsState() 监听 StateFlow。
    渲染 UI‌:根据 state 显示内容。
    传递事件‌:用户点击时调用 viewModel.increment(),‌不直接修改状态‌。
    无业务逻辑‌:UI 不关心如何计算,只负责显示结果。
    对比传统错误写法‌
    kotlin
    Copy Code
    // ❌ 错误示例:逻辑与 UI 混杂
    @Composable
    fun BadCounter() {
    var count by remember { mutableStateOf(0) }

    Column(…) {
    Text(“Count: $count”)
    Button(onClick = { count++ }) { // ❗️ 业务逻辑直接写在 UI 层
    Text(“Add +1”)
    }
    }
    }

问题‌:业务逻辑(计数器逻辑)直接嵌入 UI 层,无法复用且难以测试。
关键原则‌

单向数据流‌:
UI → 发送事件 → ViewModel → 更新状态 → UI 响应‌

UI 无状态化‌:
Composable 只持有 View 相关的临时状态(如动画进度),业务状态全部交给 ViewModel。

可测试性‌:
可单独测试 ViewModel 的逻辑,无需依赖 UI 框架。

进阶场景‌

如果应用需要处理异步操作(如网络请求),进一步分层:

kotlin
Copy Code
class UserViewModel(
private val userRepository: UserRepository // 数据源抽象
) : ViewModel() {
private val _state = MutableStateFlow(UserState())
val state: StateFlow = _state.asStateFlow()

fun loadUser(userId: String) {viewModelScope.launch {_state.update { it.copy(isLoading = true) }val user = userRepository.getUser(userId) // 调用仓库层_state.update { it.copy(user = user, isLoading = false) }}
}

}

通过这种分层架构,Compose 代码保持简洁,‌逻辑冗余‌和‌维护成本‌将显著降低!

那如果在上面的例子中的数据层加入网络请求,该怎么写?

以下是在原有计数器示例中‌加入网络请求层‌的完整实现,采用 ‌Repository 模式 + ViewModel + StateFlow‌ 架构,结合搜索结果中提到的协程与 Retrofit 实践‌:

分层架构与代码实现‌

  1. 网络请求层(Retrofit 接口)‌
    kotlin
    Copy Code
    // 📁 network/ApiService.kt
    interface ApiService {
    @GET(“api/counter”)
    suspend fun getCounter(): Response // 模拟获取初始值

    @POST(“api/counter”)
    suspend fun saveCounter(@Body count: Int): Response // 模拟保存
    }

  2. 数据层(Repository)‌
    kotlin
    Copy Code
    // 📁 repository/CounterRepository.kt
    class CounterRepository(private val api: ApiService) {
    // 封装网络请求与本地逻辑
    suspend fun loadInitialCount(): Int {
    return try {
    val response = api.getCounter()
    if (response.isSuccessful) response.body() ?: 0 else 0
    } catch (e: Exception) {
    0 // 网络异常返回默认值
    }
    }

    suspend fun saveCount(count: Int): Boolean {
    return try {
    api.saveCounter(count).isSuccessful
    } catch (e: Exception) {
    false
    }
    }
    }

  3. 状态层(扩展原有 CounterState)‌
    kotlin
    Copy Code
    // 📁 state/CounterState.kt
    data class CounterState(
    val count: Int = 0,
    val isLoading: Boolean = false, // 加载状态
    val error: String? = null // 错误信息
    )

  4. 逻辑层(ViewModel 改造)‌
    kotlin
    Copy Code
    // 📁 viewmodel/CounterViewModel.kt
    class CounterViewModel(
    private val repository: CounterRepository
    ) : ViewModel() {
    private val _state = MutableStateFlow(CounterState())
    val state: StateFlow = _state.asStateFlow()

    // 初始化时加载网络数据
    init {
    loadInitialCount()
    }

    private fun loadInitialCount() {
    viewModelScope.launch {
    _state.update { it.copy(isLoading = true) }
    val initialCount = repository.loadInitialCount()
    _state.update { it.copy(count = initialCount, isLoading = false) }
    }
    }

    fun increment() {
    _state.update { it.copy(count = it.count + 1) }
    saveCount() // 每次修改后自动保存
    }

    private fun saveCount() {
    viewModelScope.launch {
    _state.update { it.copy(isLoading = true) }
    val success = repository.saveCount(state.value.count)
    _state.update {
    it.copy(
    isLoading = false,
    error = if (!success) “保存失败” else null
    )
    }
    }
    }
    }

  5. UI 层(Composable 适配状态)‌
    kotlin
    Copy Code
    // 📁 ui/CounterScreen.kt
    @Composable
    fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
    val state by viewModel.state.collectAsState()

    Box(modifier = Modifier.fillMaxSize()) {
    Column(
    modifier = Modifier.align(Alignment.Center),
    horizontalAlignment = Alignment.CenterHorizontally
    ) {
    if (state.isLoading) {
    CircularProgressIndicator() // 加载指示器
    } else {
    Text(text = “当前计数: ${state.count}”, fontSize = 24.sp)
    }

         Button(onClick = { viewModel.increment() },enabled = !state.isLoading // 加载时禁用按钮) {Text("增加计数")}state.error?.let { Text(text = it, color = Color.Red) // 错误提示}}
    

    }
    }

关键实现说明‌

分层架构‌

网络层‌:Retrofit 定义 API 接口‌
数据层‌:Repository 封装网络请求与本地逻辑‌
逻辑层‌:ViewModel 通过协程管理异步操作,更新 StateFlow 状态‌
UI 层‌:Composable 仅观察状态并发送事件‌

状态管理‌

使用 CounterState 集中管理 ‌加载状态‌、‌错误信息‌ 和 ‌核心数据‌‌
通过 viewModelScope 自动取消协程,避免内存泄漏‌

错误处理‌

使用 try-catch 捕获网络异常‌
通过 error 字段传递错误信息到 UI 层‌
依赖注入示例(简化版)‌
kotlin
Copy Code
// 📁 di/AppModule.kt (使用 Koin 示例)
val appModule = module {
single { Retrofit.Builder().baseUrl(“https://your-api.com/”).build() }
single { get().create(ApiService::class.java) }
single { CounterRepository(get()) }
viewModel { CounterViewModel(get()) }
}

通过这种设计,网络请求逻辑被完全隔离在数据层,UI 层只需处理状态渲染,符合 MVVM 架构的最佳实践‌。

上面的示例中如果compose里面的ui修改了数据,如何写回数据层?

在 Compose 中修改数据并写回数据层,需要遵循 ‌单向数据流‌ 原则:UI 层只负责触发事件,由 ViewModel 协调业务逻辑并更新数据层。以下是具体实现流程和示例:

数据修改流程‌
text
Copy Code
UI 触发事件 → ViewModel 处理逻辑 → Repository 操作数据层 → 更新 State → UI 刷新

场景示例:用户通过输入框修改数据‌

我们扩展之前的计数器,允许用户手动输入数值并保存到服务器。

  1. 修改 State(增加输入相关状态)
    kotlin
    Copy Code
    // State.kt
    data class CounterState(
    val count: Int = 0,
    val inputText: String = “”, // 用户输入内容
    val isLoading: Boolean = false,
    val error: String? = null
    )

  2. 修改 ViewModel(新增输入处理方法)
    kotlin
    Copy Code
    // CounterViewModel.kt
    class CounterViewModel(private val repo: CounterRepository) : ViewModel() {
    private val _state = MutableStateFlow(CounterState())
    val state: StateFlow = _state.asStateFlow()

    // 处理输入框内容变化
    fun onInputChange(newText: String) {
    _state.update { it.copy(inputText = newText) }
    }

    // 提交用户输入的值
    fun submitCustomCount() {
    val newCount = state.value.inputText.toIntOrNull() ?: run {
    _state.update { it.copy(error = “请输入有效数字”) }
    return
    }

     viewModelScope.launch {_state.update { it.copy(isLoading = true) }val success = repo.saveCount(newCount)_state.update {if (success) {it.copy(count = newCount, isLoading = false, inputText = "")} else {it.copy(error = "保存失败", isLoading = false)}}}
    

    }
    }

  3. 修改 UI 层(增加输入框)
    kotlin
    Copy Code
    // CounterScreen.kt
    @Composable
    fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
    val state by viewModel.state.collectAsState()

    Column(Modifier.padding(16.dp)) {
    // 显示当前计数
    Text(“当前计数: ${state.count}”, style = MaterialTheme.typography.h6)

     // 输入框OutlinedTextField(value = state.inputText,onValueChange = viewModel::onInputChange, // 输入变化触发事件label = { Text("输入新数值") },keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number))// 提交按钮Button(onClick = { viewModel.submitCustomCount() },enabled = !state.isLoading) {if (state.isLoading) {CircularProgressIndicator(Modifier.size(20.dp))} else {Text("手动更新")}}// 错误提示state.error?.let {Text(it, color = Color.Red)}
    

    }
    }

关键机制解释‌

输入处理‌

用户输入时,onValueChange 触发 viewModel.onInputChange,更新 inputText 状态
不直接修改数据层‌,仅记录临时输入内容

数据提交‌

点击按钮时,调用 viewModel.submitCustomCount()
ViewModel 进行‌数据验证‌(检查是否为有效数字)
通过 Repository ‌保存到网络‌,成功后更新 count 状态

状态同步‌

数据保存成功后,count 更新会自动触发 UI 重组
输入框内容清空 (inputText = “”)
架构优势‌
层级 职责 修改数据的入口
UI 层‌ 展示数据、收集用户输入事件 仅调用 viewModel.xxx()
ViewModel‌ 业务逻辑处理、状态管理 修改 StateFlow 并操作 Repository
Repository‌ 统一数据源访问(网络/本地) 提供 saveCount() 等原子操作
处理复杂场景‌

如果需要‌本地缓存 + 网络同步‌,可在 Repository 中添加逻辑:

kotlin
Copy Code
// CounterRepository.kt
class CounterRepository(
private val api: ApiService,
private val localDataSource: CounterLocalDataSource
) {
suspend fun saveCount(count: Int): Boolean {
return try {
// 先保存到本地
localDataSource.save(count)
// 再同步到网络
api.saveCounter(count).isSuccessful
} catch (e: Exception) {
false
}
}
}

通过这种设计,Compose UI 修改数据的完整链路被严格约束在架构规范内,‌数据流向清晰‌且‌易于维护扩展‌。

那前面我说的依赖注入是什么意思,如果你不太理解,我详细讲讲

依赖注入(Dependency Injection, DI)是一种设计模式,用于‌解耦代码‌并‌提高可维护性‌。通过外部提供组件所需的依赖,而不是让组件自己创建依赖,使得代码更灵活、更易测试。以下结合你的示例逐步解释:

  1. 什么是依赖注入?‌

想象一个咖啡机需要咖啡豆:

❌ ‌传统方式‌:咖啡机内部自己种植咖啡豆 → ‌高耦合‌
✅ ‌依赖注入‌:外部提供咖啡豆给咖啡机 → ‌解耦‌

代码中的类比:

ViewModel 需要 Repository
Repository 需要 ApiService
依赖注入负责将这些依赖“传递”给需要它们的组件。
2. 为什么需要依赖注入?‌
问题示例(无依赖注入)‌
kotlin
Copy Code
// ❌ 直接在 ViewModel 内部创建 Repository
class CounterViewModel : ViewModel() {
private val repo = CounterRepository() // 直接创建依赖
// …
}

// ❌ Repository 内部创建 ApiService
class CounterRepository {
private val api = Retrofit.Builder().build().create(ApiService::class.java)
// …
}

痛点‌:

组件高度耦合,难以替换实现(如测试时无法替换为 Mock 接口)。
重复创建对象(如多个组件需要同一个 ApiService 实例)。
3. 如何实现依赖注入?‌
(1) 手动依赖注入(不借助框架)‌
kotlin
Copy Code
// 步骤1:外部创建依赖
val apiService = Retrofit.Builder().build().create(ApiService::class.java)
val repository = CounterRepository(apiService) // 注入 ApiService
val viewModel = CounterViewModel(repository) // 注入 Repository

// 步骤2:通过构造函数传递依赖
class CounterViewModel(private val repo: CounterRepository) : ViewModel() {
// 业务逻辑使用 repo
}

class CounterRepository(private val api: ApiService) {
// 数据操作使用 api
}

(2) 使用依赖注入框架(如 Koin/Hilt)‌

以 Koin 为例:

kotlin
Copy Code
// 📁 di/AppModule.kt
val appModule = module {
// 定义如何创建 ApiService(单例)
single { Retrofit.Builder().build().create(ApiService::class.java) }

// 定义如何创建 Repository,注入 ApiService
single { CounterRepository(get()) } // get() 会自动查找已注册的 ApiService// 定义 ViewModel,注入 Repository
viewModel { CounterViewModel(get()) }

}

// 📁 MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 初始化 Koin
startKoin { modules(appModule) }
setContent { CounterScreen() }
}
}

  1. 在你的示例中如何工作?‌

定义依赖关系‌:
Koin 模块声明 ApiService → Repository → ViewModel 的创建顺序。

自动注入‌:
viewModel { CounterViewModel(get()) } 中的 get() 会自动查找并注入已注册的 CounterRepository。

在 Composable 中获取 ViewModel‌:

kotlin
Copy Code
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
// Koin 会自动提供已注入依赖的 ViewModel 实例
}

  1. 依赖注入的优势‌
    场景 无依赖注入 有依赖注入
    替换实现‌ 需修改多处代码 只需修改模块定义
    单元测试‌ 难以 Mock 依赖 轻松替换为测试依赖(如 Mock 对象)
    代码复用‌ 重复创建相同对象 单例管理,全局复用
    生命周期管理‌ 手动管理 框架自动管理(如 ViewModel 生命周期)
  2. 常见问题‌
    Q:一定要用 Koin/Hilt 吗?‌
    不是!小型项目可以手动注入,但框架能简化大型项目的依赖管理。
    Q:依赖注入会增加复杂度吗?‌
    初期学习成本略高,但长期大幅提升代码可维护性。
    Q:Compose 中如何配合使用?‌
    通过 viewModel() 或 hiltViewModel() 自动获取已注入依赖的 ViewModel。

通过依赖注入,你的示例中的 ViewModel、Repository 和 ApiService 完全解耦,各组件职责清晰,为应用扩展(如添加数据库、Mock 测试)打下坚实基础。

上面如果换成用hilt依赖注入是怎么样的?

好的!我们使用 ‌Hilt‌(基于 Dagger 的 Android 官方 DI 框架)替代 Koin,重构之前的依赖注入实现。以下是详细步骤和代码对比:

  1. 添加 Hilt 依赖‌

在 build.gradle 中添加必要依赖:

groovy
Copy Code
// 根目录 build.gradle
plugins {
id ‘com.google.dagger.hilt.android’ version ‘2.48’ apply false
}

// App 模块 build.gradle
plugins {
id ‘com.google.dagger.hilt.android’
}

dependencies {
implementation “com.google.dagger:hilt-android:2.48”
kapt “com.google.dagger:hilt-android-compiler:2.48”
implementation ‘androidx.hilt:hilt-navigation-compose:1.0.0’ // Compose 支持
}

  1. 定义 Hilt 模块‌
    Retrofit 模块‌
    kotlin
    Copy Code
    // 📁 di/NetworkModule.kt
    @Module
    @InstallIn(SingletonComponent::class) // 全局单例
    object NetworkModule {
    @Provides
    fun provideApiService(): ApiService {
    return Retrofit.Builder()
    .baseUrl(“https://your-api.com/”)
    .build()
    .create(ApiService::class.java)
    }
    }

Repository 模块‌
kotlin
Copy Code
// 📁 di/RepositoryModule.kt
@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {
@Provides
fun provideCounterRepository(api: ApiService): CounterRepository {
return CounterRepository(api)
}
}

  1. 修改 Application 类‌
    kotlin
    Copy Code
    // 📁 MyApplication.kt
    @HiltAndroidApp // 必须标记
    class MyApplication : Application()

在 AndroidManifest.xml 中声明:

xml
Copy Code
<application
android:name=“.MyApplication”
…>

  1. 改造 ViewModel (使用 Hilt 注入)‌
    kotlin
    Copy Code
    // 📁 viewmodel/CounterViewModel.kt
    @HiltViewModel // 替换 Koin 的 viewModel 定义
    class CounterViewModel @Inject constructor(
    private val repository: CounterRepository // 自动注入
    ) : ViewModel() {
    // 原有逻辑不变
    }

  2. 在 Compose 中获取 ViewModel‌
    kotlin
    Copy Code
    // 📁 ui/CounterScreen.kt
    @Composable
    fun CounterScreen(
    viewModel: CounterViewModel = hiltViewModel() // 替换 Koin 的 viewModel()
    ) {
    val state by viewModel.state.collectAsState()
    // 原有 UI 逻辑不变
    }

  3. 对比 Hilt 与 Koin 的关键区别‌
    特性 Hilt (Dagger) Koin
    原理‌ 编译时生成代码(APT + KSP) 运行时反射 + 函数式注册
    性能‌ 编译时处理,无运行时开销 运行时解析,轻微性能损耗
    学习曲线‌ 较陡峭(需理解 Dagger 注解) 简单(类似 Service Locator)
    代码侵入性‌ 需要 @HiltAndroidApp 和 @AndroidEntryPoint 无侵入,纯 Kotlin 实现
    Google 官方支持‌ 是 否

  4. Hilt 核心注解说明‌
    注解 作用
    @HiltAndroidApp 标记 Application 类,触发 Hilt 代码生成
    @AndroidEntryPoint 标记 Activity/Fragment 等 Android 组件,允许注入依赖(Compose 中用 hiltViewModel() 替代)
    @HiltViewModel 标记 ViewModel,允许通过构造函数注入依赖
    @Module + @InstallIn 定义依赖提供模块,并指定作用域(如 SingletonComponent 表示全局单例)
    @Provides 在模块中标记提供依赖的方法
    @Inject 标记构造函数或字段,表示需要注入依赖

  5. 处理复杂依赖场景‌
    需要 Context 的依赖(示例)‌
    kotlin
    Copy Code
    @Module
    @InstallIn(SingletonComponent::class)
    object ContextModule {
    @Provides
    fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
    return context.getSharedPreferences(“my_prefs”, Context.MODE_PRIVATE)
    }
    }

接口与实现类绑定‌
kotlin
Copy Code
// 定义接口
interface DataSource {
fun fetchData(): String
}

// 实现类
class NetworkDataSource @Inject constructor() : DataSource {
override fun fetchData() = “Data from network”
}

// 模块绑定
@Module
@InstallIn(SingletonComponent::class)
interface DataSourceModule {
@Binds
fun bindDataSource(networkDS: NetworkDataSource): DataSource
}

  1. 测试中的依赖替换‌

在单元测试中,可以用 Hilt 的测试支持替换模块:

kotlin
Copy Code
// 测试模块
@Module
@TestInstallIn(components = [SingletonComponent::class], replaces = [NetworkModule::class])
object FakeNetworkModule {
@Provides
fun provideFakeApiService(): ApiService {
return object : ApiService { // 模拟实现
override suspend fun getCounter() = Response.success(100)
}
}
}

// 测试类
@HiltAndroidTest
class CounterTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)

@Before
fun init() {hiltRule.inject()
}@Test
fun testCounterInitialValue() {val vm = hiltViewModel<CounterViewModel>()assert(vm.state.value.count == 100) // 使用模拟数据
}

}

通过 Hilt 实现依赖注入,你的应用将获得:

编译时安全性‌:依赖关系在编译时检查,避免运行时错误
高效性能‌:无反射开销
标准化架构‌:符合 Google 推荐的 Android 开发最佳实践

在大型项目中,Hilt 的强类型和编译时检查优势尤为明显,建议优先选择 Hilt 作为 DI 框架。

hilt依赖注入没使用反射吗?那它是什么原理生成的
Hilt 的依赖注入机制‌不依赖反射‌,其核心原理是通过‌编译时代码生成‌实现的‌。以下是具体实现原理的分解:

一、Hilt 的核心机制‌

  1. ‌注解处理‌
    编译时扫描‌:Hilt 在编译阶段通过注解处理器(APT/KSP)扫描代码中的注解(如 @HiltAndroidApp、@HiltViewModel 等)‌。
    生成代码模板‌:根据注解生成对应的 Dagger 组件(Component)和模块(Module)实现类,例如 Hilt_MyApplication、Hilt_CounterViewModel 等‌。
  2. ‌组件与模块管理‌
    组件层级‌:Hilt 预定义了 Android 组件的层级(如 SingletonComponent、ActivityComponent),每个层级对应不同的生命周期范围‌。
    依赖绑定‌:通过 @Module 和 @Provides 定义依赖提供方式,Hilt 自动将依赖注入到目标类(如 ViewModel、Activity)的构造函数或字段中‌。
  3. ‌代码生成与编译优化‌
    静态代码生成‌:Hilt 在编译时生成所有依赖注入相关的代码(如 Factory 类、Component 接口实现),运行时直接调用生成的代码完成依赖注入‌。
    类型安全检查‌:所有依赖关系在编译时完成验证,避免运行时因类型不匹配导致的错误‌。
    二、Hilt 与反射的对比‌
    特性‌ ‌Hilt(Dagger 2)‌ ‌反射方案(如 Koin)‌
    实现方式‌ 编译时生成代码 运行时通过反射解析依赖
    性能‌ 无运行时开销,效率高‌ 存在反射调用开销,效率略低
    类型安全‌ 编译时检查,避免类型错误‌ 运行时可能因类型问题崩溃
    可维护性‌ 代码生成逻辑清晰,但需理解 Dagger 注解 配置简单,但依赖关系隐式管理
    三、Hilt 的工作流程示例‌

以 @HiltViewModel 为例:

编译阶段‌:
Hilt 检测到 @HiltViewModel 注解,生成 Hilt_CounterViewModel 类及对应的 ViewModelFactory‌。
生成 DaggerAppComponent 类,包含依赖注入的完整逻辑‌。
运行时阶段‌:
应用启动时,@HiltAndroidApp 注解触发全局依赖容器的初始化‌。
在 Activity 中调用 hiltViewModel() 时,直接使用预生成的工厂类实例化 ViewModel,无需反射‌。
四、优势总结‌
高性能‌:完全避免反射,依赖解析在编译期完成‌。
强类型安全‌:所有依赖关系编译时验证,减少运行时崩溃风险‌。
标准化架构‌:通过预定义组件层级,强制规范代码结构‌。

Hilt 的设计充分体现了‌编译时优化‌和‌类型安全优先‌的理念,是大型 Android 项目依赖注入的优选方案‌。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/37104.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微信小程序的业务域名配置(通过ingress网关的注解)

一、背景 微信小程序的业务域名配置&#xff08;通过kong网关的pre-function配置&#xff09;是依靠kong实现&#xff0c;本文将通过ingress网关实现。 而我们的服务是部署于阿里云K8S容器&#xff0c;当然内核与ingress无异。 找到k8s–>网络–>路由 二、ingress注解 …

LiteratureReading:[2016] Enriching Word Vectors with Subword Information

文章目录 一、文献简明&#xff08;zero&#xff09;二、快速预览&#xff08;first&#xff09;1、标题分析2、作者介绍3、引用数4、摘要分析&#xff08;1&#xff09;翻译&#xff08;2&#xff09;分析 5、总结分析&#xff08;1&#xff09;翻译&#xff08;2&#xff09;…

前后端联调解决跨域问题的方案

引言 在前后端分离的开发模式中&#xff0c;前端和后端通常在不同的服务器或端口运行&#xff0c;这样就会面临跨域问题。跨域问题是指浏览器因安全限制阻止前端代码访问与当前网页源不同的域、协议或端口的资源。对于 Java 后端应用&#xff0c;我们可以通过配置 CORS&#x…

开源软件许可证冲突的原因和解决方法

1、什么是开源许可证以及许可证冲突产生的问题 开源软件许可证是一种法律文件&#xff0c;它规定了软件用户、分发者和修改者使用、复制、修改和分发开源软件的权利和义务。开源许可证是由软件的版权所有者&#xff08;通常是开发者或开发团队&#xff09;发布的&#xff0c;它…

python爬虫笔记(一)

文章目录 html基础标签和下划线无序列表和有序列表表格加边框 html的属性a标签&#xff08;网站&#xff09;target属性换行线和水平分割线 图片设置宽高width&#xff0c;height html区块——块元素与行内元素块元素与行内元素块元素举例行内元素举例 表单from标签type属性pla…

电脑节电模式怎么退出 分享5种解决方法

在使用电脑的过程中&#xff0c;许多用户为了节省电力&#xff0c;通常会选择开启电脑的节能模式。然而&#xff0c;在需要更高性能或进行图形密集型任务时&#xff0c;节能模式可能会限制系统的性能表现。这时&#xff0c;了解如何正确地关闭或调整节能设置就显得尤为重要了。…

AI学习——卷积神经网络(CNN)入门

作为人类&#xff0c;我们天生擅长“看”东西&#xff1a;一眼就能认出猫狗、分辨红绿灯、读懂朋友的表情……但计算机的“眼睛”最初是一片空白。直到卷积神经网络&#xff08;CNN&#xff09;​的出现&#xff0c;计算机才真正开始理解图像。今天&#xff0c;我们就用最通俗的…

2025年渗透测试面试题总结- shopee-安全工程师(题目+回答)

网络安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 shopee-安全工程师 信息安全相关Response头详解 1. 关键安全头及防御场景 Linux与Docker核心命令速查…

IntelliJ IDEA 中 Maven 的 `pom.xml` 变灰带横线?一文详解解决方法

前言 在使用 IntelliJ IDEA 进行 Java 开发时&#xff0c;如果你发现项目的 pom.xml 文件突然变成灰色并带有删除线&#xff0c;这可能是 Maven 的配置或项目结构出现了问题。 一、问题现象与原因分析 现象描述 文件变灰&#xff1a;pom.xml 在项目资源管理器中显示为灰色。…

Spring MVC 接口数据

访问路径设置 RequestMapping("springmvc/hello") 就是用来向handlerMapping中注册的方法注解! 秘书中设置路径和方法的对应关系&#xff0c;即RequestMapping("/springmvc/hello")&#xff0c;设置的是对外的访问地址&#xff0c; 路径设置 精准路径匹…

技术分享 | MySQL内存使用率高问题排查

本文为墨天轮数据库管理服务团队第51期技术分享&#xff0c;内容原创&#xff0c;如需转载请联系小墨&#xff08;VX&#xff1a;modb666&#xff09;并注明来源。 一、问题现象 问题实例mysql进程实际内存使用率过高 二、问题排查 2.1 参数检查 mysql版本 &#xff1a;8.0.…

【redis】什么是持久化之 RDB

什么是持久化 MySQL 的事务&#xff0c;有四个比较核心的特性&#xff1a; 原子性一致性持久性>持久化&#xff08;说的一回事&#xff09; 把数据存储在硬盘上>持久把数据存在内存上>不持久重启进程/重启主机之后&#xff0c;数据是否还存在 隔离性 Redis 是一个内存…

Python、MATLAB和PPT完成数学建模竞赛中的地图绘制

参加数学建模比赛时&#xff0c;很多题目——诸如统计类、数据挖掘类、环保类、建议类的题目总会涉及到地理相关的情景&#xff0c;往往要求我们制作与地图相关的可视化内容。如下图&#xff0c;这是21年亚太赛的那道塞罕坝的题目&#xff0c;期间涉及到温度、降水和森林覆盖率…

Python(冒泡排序、选择排序、插入法排序、快速排序,算法稳定性)

算法的稳定性 冒泡排序 # 冒泡排序 # 1 思想: 相邻位置两个元素比较, 前面的元素比后面的元素大则交换, 把最大的数给找到 # 经过一轮一轮的比较最终把序列给排序 # 2 关键点1: 两层for循环 外层循环控制多少轮 内层for循环控制比较次数 # 3 关键点2: 若遍历一遍没有数字…

【自用】NLP算法面经(5)

一、L1、L2正则化 正则化是机器学习中用于防止过拟合并提高模型泛化能力的技术。当模型过拟合时&#xff0c;它已经很好地学习了训练数据&#xff0c;甚至是训练数据中的噪声&#xff0c;所以可能无法在新的、未见过的数据上表现良好。 比如&#xff1a; 其中&#xff0c;x1和…

PyCharm安装redis,python安装redis,PyCharm使用失败问题

报错信息 Usage: D:\wb2\wbrj_pys\venv\Scripts\python.exe -m pip install [options] [package-index-options] … D:\wb2\wbrj_pys\venv\Scripts\python.exe -m pip install [options] -r [package-index-options] … D:\wb2\wbrj_pys\venv\Scripts\python.exe -m pip instal…

学习笔记|arduino uno r3|DS1307时钟芯片|Atmega328P| 设置时间|读取时间|无源晶振:DS1307时钟芯片实验

目录 芯片pinout&#xff1a; 实验器件&#xff1a; 实验连线 解决AVR 架构不支持 printf() 方法 使用GetTimeAndDate.ino设置时间&#xff1a; 使用SetTimeAndDate.ino设置时间&#xff1a; 芯片pinout&#xff1a; DS1307 是美国 DALLAS 公司推出的 I 总线接口实时时钟芯…

uniapp可拖拽消息数徽标draggable-badge,仿手机qq聊天列表未读数徽标动效

组件下载地址&#xff1a;https://ext.dcloud.net.cn/plugin?id22679 兼容性&#xff1a; 测试了h5和微信小程序&#xff0c;理论支持全平台&#xff0c;暂不支持pc端&#xff0c;不过可以自己修改事件兼容pc 使用uniapp仿写了一个手机qq聊天列表右侧未读数的徽标组件&#x…

【设计模式】策略模式

以下是格式优化后的Markdown文档&#xff0c;仅调整代码缩进&#xff0c;保持内容不变&#xff1a; 四、策略模式 策略(Strategy) 模式是一种行为型模式&#xff0c;其实现过程与模板方法模式非常类似——都 是以扩展的方式支持未来的变化。本章通过对一个具体范例的逐步重构…

STM32配套程序接线图

1 工程模板 2 LED闪烁 3LED流水灯 4蜂鸣器 5按键控制LED 6光敏传感器控制蜂鸣器 7OLED显示屏 8对射式红外传感器计次 9旋转编码器计次 10 定时器定时中断 11定时器外部时钟 12PWM驱动LED呼吸灯 13 PWM驱动舵机 14 PWM驱动直流电机 15输入捕获模式测频率 16PWMI模式测频率占空…