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")}
}
}
关键解析
-
数据层(CounterState)
定义所有 UI 需要显示的状态(如 count 和 isLoading)。
作用:集中管理 UI 状态,避免分散在 Composable 中。 -
逻辑层(CounterViewModel)
职责:处理业务逻辑(如计算、网络请求等)。
StateFlow:通过 StateFlow 暴露状态,确保 UI 能监听状态变化。
事件处理:increment() 和 reset() 方法只修改状态,不涉及 UI 操作。 -
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 实践:
分层架构与代码实现
-
网络请求层(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 // 模拟保存
} -
数据层(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
}
}
} -
状态层(扩展原有 CounterState)
kotlin
Copy Code
// 📁 state/CounterState.kt
data class CounterState(
val count: Int = 0,
val isLoading: Boolean = false, // 加载状态
val error: String? = null // 错误信息
) -
逻辑层(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
)
}
}
}
} -
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 刷新
场景示例:用户通过输入框修改数据
我们扩展之前的计数器,允许用户手动输入数值并保存到服务器。
-
修改 State(增加输入相关状态)
kotlin
Copy Code
// State.kt
data class CounterState(
val count: Int = 0,
val inputText: String = “”, // 用户输入内容
val isLoading: Boolean = false,
val error: String? = null
) -
修改 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)}}}}
} -
修改 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)是一种设计模式,用于解耦代码并提高可维护性。通过外部提供组件所需的依赖,而不是让组件自己创建依赖,使得代码更灵活、更易测试。以下结合你的示例逐步解释:
- 什么是依赖注入?
想象一个咖啡机需要咖啡豆:
❌ 传统方式:咖啡机内部自己种植咖啡豆 → 高耦合
✅ 依赖注入:外部提供咖啡豆给咖啡机 → 解耦
代码中的类比:
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() }
}
}
- 在你的示例中如何工作?
定义依赖关系:
Koin 模块声明 ApiService → Repository → ViewModel 的创建顺序。
自动注入:
viewModel { CounterViewModel(get()) } 中的 get() 会自动查找并注入已注册的 CounterRepository。
在 Composable 中获取 ViewModel:
kotlin
Copy Code
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
// Koin 会自动提供已注入依赖的 ViewModel 实例
}
- 依赖注入的优势
场景 无依赖注入 有依赖注入
替换实现 需修改多处代码 只需修改模块定义
单元测试 难以 Mock 依赖 轻松替换为测试依赖(如 Mock 对象)
代码复用 重复创建相同对象 单例管理,全局复用
生命周期管理 手动管理 框架自动管理(如 ViewModel 生命周期) - 常见问题
Q:一定要用 Koin/Hilt 吗?
不是!小型项目可以手动注入,但框架能简化大型项目的依赖管理。
Q:依赖注入会增加复杂度吗?
初期学习成本略高,但长期大幅提升代码可维护性。
Q:Compose 中如何配合使用?
通过 viewModel() 或 hiltViewModel() 自动获取已注入依赖的 ViewModel。
通过依赖注入,你的示例中的 ViewModel、Repository 和 ApiService 完全解耦,各组件职责清晰,为应用扩展(如添加数据库、Mock 测试)打下坚实基础。
上面如果换成用hilt依赖注入是怎么样的?
好的!我们使用 Hilt(基于 Dagger 的 Android 官方 DI 框架)替代 Koin,重构之前的依赖注入实现。以下是详细步骤和代码对比:
- 添加 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 支持
}
- 定义 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)
}
}
- 修改 Application 类
kotlin
Copy Code
// 📁 MyApplication.kt
@HiltAndroidApp // 必须标记
class MyApplication : Application()
在 AndroidManifest.xml 中声明:
xml
Copy Code
<application
android:name=“.MyApplication”
…>
-
改造 ViewModel (使用 Hilt 注入)
kotlin
Copy Code
// 📁 viewmodel/CounterViewModel.kt
@HiltViewModel // 替换 Koin 的 viewModel 定义
class CounterViewModel @Inject constructor(
private val repository: CounterRepository // 自动注入
) : ViewModel() {
// 原有逻辑不变
} -
在 Compose 中获取 ViewModel
kotlin
Copy Code
// 📁 ui/CounterScreen.kt
@Composable
fun CounterScreen(
viewModel: CounterViewModel = hiltViewModel() // 替换 Koin 的 viewModel()
) {
val state by viewModel.state.collectAsState()
// 原有 UI 逻辑不变
} -
对比 Hilt 与 Koin 的关键区别
特性 Hilt (Dagger) Koin
原理 编译时生成代码(APT + KSP) 运行时反射 + 函数式注册
性能 编译时处理,无运行时开销 运行时解析,轻微性能损耗
学习曲线 较陡峭(需理解 Dagger 注解) 简单(类似 Service Locator)
代码侵入性 需要 @HiltAndroidApp 和 @AndroidEntryPoint 无侵入,纯 Kotlin 实现
Google 官方支持 是 否 -
Hilt 核心注解说明
注解 作用
@HiltAndroidApp 标记 Application 类,触发 Hilt 代码生成
@AndroidEntryPoint 标记 Activity/Fragment 等 Android 组件,允许注入依赖(Compose 中用 hiltViewModel() 替代)
@HiltViewModel 标记 ViewModel,允许通过构造函数注入依赖
@Module + @InstallIn 定义依赖提供模块,并指定作用域(如 SingletonComponent 表示全局单例)
@Provides 在模块中标记提供依赖的方法
@Inject 标记构造函数或字段,表示需要注入依赖 -
处理复杂依赖场景
需要 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
}
- 测试中的依赖替换
在单元测试中,可以用 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 的核心机制
- 注解处理
编译时扫描:Hilt 在编译阶段通过注解处理器(APT/KSP)扫描代码中的注解(如 @HiltAndroidApp、@HiltViewModel 等)。
生成代码模板:根据注解生成对应的 Dagger 组件(Component)和模块(Module)实现类,例如 Hilt_MyApplication、Hilt_CounterViewModel 等。 - 组件与模块管理
组件层级:Hilt 预定义了 Android 组件的层级(如 SingletonComponent、ActivityComponent),每个层级对应不同的生命周期范围。
依赖绑定:通过 @Module 和 @Provides 定义依赖提供方式,Hilt 自动将依赖注入到目标类(如 ViewModel、Activity)的构造函数或字段中。 - 代码生成与编译优化
静态代码生成:Hilt 在编译时生成所有依赖注入相关的代码(如 Factory 类、Component 接口实现),运行时直接调用生成的代码完成依赖注入。
类型安全检查:所有依赖关系在编译时完成验证,避免运行时因类型不匹配导致的错误。
二、Hilt 与反射的对比
特性 Hilt(Dagger 2) 反射方案(如 Koin)
实现方式 编译时生成代码 运行时通过反射解析依赖
性能 无运行时开销,效率高 存在反射调用开销,效率略低
类型安全 编译时检查,避免类型错误 运行时可能因类型问题崩溃
可维护性 代码生成逻辑清晰,但需理解 Dagger 注解 配置简单,但依赖关系隐式管理
三、Hilt 的工作流程示例
以 @HiltViewModel 为例:
编译阶段:
Hilt 检测到 @HiltViewModel 注解,生成 Hilt_CounterViewModel 类及对应的 ViewModelFactory。
生成 DaggerAppComponent 类,包含依赖注入的完整逻辑。
运行时阶段:
应用启动时,@HiltAndroidApp 注解触发全局依赖容器的初始化。
在 Activity 中调用 hiltViewModel() 时,直接使用预生成的工厂类实例化 ViewModel,无需反射。
四、优势总结
高性能:完全避免反射,依赖解析在编译期完成。
强类型安全:所有依赖关系编译时验证,减少运行时崩溃风险。
标准化架构:通过预定义组件层级,强制规范代码结构。
Hilt 的设计充分体现了编译时优化和类型安全优先的理念,是大型 Android 项目依赖注入的优选方案。
相关文章:
Android 关于compose的一些坑和理解
** 1.如何在 WindowManager.addView 中使用 Jetpack Compose** 一、引出问题 Android 开发中,很常见的一个场景,通过 WindowManager.addView() 添加一个 View 到屏幕上。Android 最新的视图框架 Jetpack Compose,如何应用进来。这个被添加的…...
LeetCode 30 —— 30.串联所有单词的子串
题目: 给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。 注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。 示例 1ÿ…...
go语言中的strings库
strings库 func EqualFold func EqualFold(s, t string) bool判断两个utf-8编码字符串(将unicode大写、小写、标题三种格式字符视为相同)是否相同。 func main() {fmt.Println(strings.EqualFold("hello", "hello")) //truefmt.…...
【嵌入式硬件】三款DCDC调试笔记
关于开关电源芯片,重点关注输入电源范围、输出电流、最低压降。 1.MP9943: 以MP9943为例,输入电压范围4-36V,输出最大电流3A,最低压降为0.3V 调整FB使正常输出为5.06V 给定6V空载、5V空载、5V带2A负载的情况: 6V带2A…...
Cannot find module @rollup/rollup-win32-x64-msvc
方法1 在package.json中添加postinstall: "scripts": {"postinstall": "node -e \"const { platform } process; if (platform win32) { require(child_process).execSync(npm install rollup/rollup-win32-x64-msvc, { stdio: inherit });…...
Linux中修改文件的权限用什么命令?
一、核心语法 chmod [选项] [权限模式] 文件名二、权限模式详解 1. 数字模式(推荐使用) 通过rwx权限对应的数值组合: r(读)→ 4w(写)→ 2x(执行)→ 1无权限 → 0 组合规…...
【FPGA开发】FPGA点亮LED灯(增加按键暂停恢复/复位操作)
目录 一、VScode下载安装 1.1 官网下载 1.2 安装插件 二、LED流水灯点亮 2.1 任务说明 2.2 新建项目 2.3 创建Verilog文件添加至顶层实体 2.4 引脚分配 2.5 选择烧录器 2.6 添加烧录文件&下载 2.7 烧录结果 三、增加按键操作 3.1 按键暂停和恢复功能&…...
companion object和object 从kotlin转java分析
说明 companion object 中 companion类中的方法是普通的方法 在外部类中生成静态变量,静态companion 对象 object MyClass2 中 类中方法是普通方法 在MyClass2中生成静态变量,静态MyClass2对象, 一 companion object 使用 kotlin转java pa…...
Spring MVC 执行流程:一个请求在 Spring MVC 中是如何执行的?
当用户发送一个 HTTP 向 Spring MVC 应用,该请求在 Spring MVC 的执行流程如下: 当用户向 Spring MVC 发起一个 HTTP 请求,该请求会被 Dispatcher Servlet(前端控制器)拦截;DispatcherServlet 调用 Handler…...
三主热备架构
1.要求 角色主机名软件IP地址用户client192.168.72.90keepalivedvip192.168.72.100masterserverAkeepalived, nginx192.168.72.30backupserverBkeepalived, nginx192.168.72.31backupserverCkeepalived, nginx192.168.72.32webtomcat1tomcat192.168.72.41webtomcat2tomcat192.1…...
HTML 表单处理进阶:验证与提交机制的学习心得与进度(二)
步步为营:表单提交机制全面解析 提交方式详解 GET 与 POST 对比 在 HTML 表单提交中,GET 和 POST 是最为常用的两种提交方式,它们在诸多方面存在显著差异。 安全性:GET 方式将表单数据附加在 URL 的查询字符串中,数…...
JavaScript | 爬虫逆向 | 语法基础| 01
一、摘要 实践是最好的导师 二、环境配置 在开始之前,需要确保你的计算机上已经安装了 Node.js。Node.js 是一个开源的、跨平台的 JavaScript 运行时环境,它允许你在服务器端运行 JavaScript 代码。 1. 下载 安装地址:https://nodejs.org…...
python解决多个矢量点图层合并为一个点图层
1、解决矢量点图层的合并 2、解决多个点图层分别合并为不同图层(一个文件夹下所有点图层合并为一个图层,以下代码为两个文件夹,分别合并为两个总的图层) import geopandas as gpd import os import pandas as pddef merge_shapef…...
VL开源模型实现文本生成图片
一、 基础知识 根据描述生成图片的视觉-语言模型(Vision-Language Models, VL 模型)是近年来多模态生成领域的热点研究方向。这些模型能够根据自然语言描述生成高质量的图像,广泛应用于艺术创作、设计辅助、虚拟场景构建等领域。 1 根据描述…...
字节跳动实习生主导开发强化学习算法,助力大语言模型性能突破
目录 禹棋赢的背景与成就 主要成就 DAPO算法的技术细节 算法优势 禹棋赢的研究历程 关键时间节点 字节跳动的“Top Seed人才计划” 计划特点 小编总结 在大模型时代,经验不再是唯一的衡量标准,好奇心、执行力和对新技术的敏锐洞察力成为推动技术…...
九、JavaScript作用域、预解析
一、JavaScript作用域 1.JavaScript作用域 ①代码名字(变量)在某个范围内起作用和效果 目的是为了提高程序的可靠性更重要的是减少命名冲突 ②js的作用域(es6)之前:全局作用域 局部作用域 ③全局作用域:整…...
前后端+数据库的项目实战:hbu迎新网-较复杂(下)javaweb
目录 十一、实现对内容的富文本编辑(换行、图片颜色等等样式) (1)下载富文本编辑器,引入资源 (2)将原项目的内容部分替换为富文本编辑器 1、替换添加页面 2、替换修改页面(和添…...
Java-模块二-2
整数类型 byte:在 Java 中占用8位(1字节),因此它的取值范围是从 -128 到 127。这是最小的整数类型,适合用于节省空间的情况。 short:这种类型的大小是16位(2字节),允许的…...
Redis、Memcached应用场景对比
环境 Redis官方网站: Redis - The Real-time Data Platform Redis社区版本下载地址:Install Redis | Docs Memcached官方网站:memcached - a distributed memory object caching system Memcached下载地址:memcached - a dis…...
【单片机通信技术应用——学习笔记三】液晶屏显示技术,取模软件的应用
一、液晶显示技术简介 1.RGB信号线 RGB是一种色彩模式,是工业界的一种颜色标准,是通过红(R)、绿(G)、蓝(B)三个颜色通道的变化,以及它们相互之间的叠加来得到各式各样的…...
MySQL颠覆版系列————MySQL新特性(开启数据库的新纪元)上篇
文章目录 前言一、窗口函数(Window Functions)1.1 窗口函数概念1.2 常见的窗口函数 二、公用表表达式(Common Table Expressions, CTEs)2.1 公用表表达式的概念2.2 常见的公用表表达式 三、JSON增强3.1 JSON增强的概念3.2 常见的J…...
MySQL 调优:查询慢除了索引还能因为什么?
文章目录 情况一:连接数过小情况二:Buffer Pool 太小 MySQL 查询慢除了索引还能因为什么?MySQL 查询慢,我们一般也会想到是因为索引,但除了索引还有哪些原因会导致数据库查询变慢呢? 以下以 MySQL 中一条 S…...
Java实习生面试题(2025.3.23 be)
一、v-if与v-show的区别 v-show 和 v-if 都是 Vue 中的条件渲染指令,它们的主要区别在于渲染策略:v-if 会根据条件决定是否编译元素,而 v-show 则始终编译元素,只是通过改变 CSS 的 display 属性来控制显示与隐藏。 二、mybatis-…...
如何在百度搜索上删除与自己名字相关的资料
个人信息的网络足迹如同一张无形的网,将我们与世界的每一个角落紧密相连。然而,当某些与自己名字相关的资料不再希望被公众轻易检索到时,如何在百度搜索中有效“隐身”,成为了一个亟待解决的问题。面对复杂多变的网络环境…...
【C语言】C语言使用随机数srand,rand
C语言使用随机数srand,rand 可直接编译使用: #include <stdio.h> #include <time.h> #include <stdlib.h> #include <unistd.h>/* c语言提供的跟随机数有关的函数:int rand(void);返回值:产生的随机数void srand(unsiqned int seed);参数…...
为容器指定固定IP地址
文章目录 为容器指定固定IP地址可以通过以下步骤实现,适用于Docker环境:**方法一:使用Docker自定义桥接网络****方法二:Docker Compose配置****关键注意事项** 为容器指定固定IP地址可以通过以下步骤实现,适用于Docker…...
kube-score K8S Yaml静态代码分析工具详解
kube-score 是一款专注于 Kubernetes 配置文件的静态代码分析工具,旨在通过自动化检查帮助用户识别资源配置中的潜在问题,并遵循最佳实践以提升集群的安全性、稳定性和性能。以下是其核心功能、使用方法和应用场景的详细解析: 一、核心功能与…...
Spring Boot 整合 Elasticsearch 实践:从入门到上手
引言 Elasticsearch 是一个开源的分布式搜索引擎,广泛用于日志分析、搜索引擎、数据分析等场景。本文将带你通过一步步的教程,在 Spring Boot 项目中整合 Elasticsearch,轻松实现数据存储与查询。 1. 创建 Spring Boot 项目 首先ÿ…...
使用外部事件检测接入 CDH 大数据管理平台告警
CDH 大数据管理平台 CDH(Cloudera Distribution Hadoop)是一个企业级的大数据平台,由 Cloudera 公司提供,它包含了 Apache Hadoop 生态系统中的多种开源组件,并对其进行了优化和集成,以支持大规模数据存储…...
RabbitMQ八股文
RabbitMQ RabbitMQ 核心概念与组件1. RabbitMQ 核心组件及其作用1.1 生产者(Producer)1.2 交换机(Exchange)1.3 队列(Queue)1.4 绑定(Binding)1.5 消费者(Consumer&#…...
