Compose + Koin ViewModel 实战完全手册

从基础注入到多页面共享、带参与避坑(可直接做学习笔记)

本篇专注于 Jetpack Compose + Koin 下 ViewModel 的正确使用姿势,覆盖基础注入、独立 VM、页面共享、带参构造、SavedStateHandle、验证共享、Navigation 嵌套导航共享、常见错误,全部为实战可运行写法,适合长期查阅。


一、前置说明

1. 作用域核心概念

  • 页面独立 VM:每个 Compose 页面单独实例,页面退出后销毁

  • 共享 VM :多个页面共用同一个实例,生命周期由 ViewModelStoreOwner 控制

    • Activity 级别:整个 App 内共享
    • NavGraph 级别:仅当前路由流程内共享
  • Compose 中获取 VM 统一使用函数调用形式

2. 依赖配置

gradle

arduino 复制代码
// Koin 核心
implementation "io.insert-koin:koin-android:3.5.3"
implementation "io.insert-koin:koin-androidx-viewmodel:3.5.3"
implementation "io.insert-koin:koin-androidx-compose:3.5.3"

// Compose 生命周期
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.7.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0"

// Compose Navigation
implementation "androidx.navigation:navigation-compose:2.7.7"

3. Koin 模块定义

kotlin

kotlin 复制代码
// 仓库
class SpeechRepository

// 普通 VM
class SpeechRecognitionViewModel(
    val repo: SpeechRepository
) : ViewModel()

// 带参数 + SavedStateHandle 的 VM
class DetailViewModel(
    val id: Int,
    val repo: SpeechRepository,
    val savedStateHandle: SavedStateHandle
) : ViewModel()

// Koin 模块
val appModule = module {
    // 单例仓库
    single { SpeechRepository() }

    // 普通 ViewModel
    viewModel { SpeechRecognitionViewModel(get()) }

    // 带参数 + 自动注入 SavedStateHandle
    viewModel { (id: Int) ->
        DetailViewModel(
            id = id,
            repo = get(),
            savedStateHandle = get() // Koin 自动提供
        )
    }
}

4. Application 初始化

kotlin

kotlin 复制代码
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@MyApp)
            modules(appModule)
        }
    }
}

二、Compose 中获取 ViewModel 的标准写法

1. 页面独立 VM(不共享)

每个页面独立实例,互不干扰,最常用。

kotlin

kotlin 复制代码
@Composable
fun SpeechPage() {
    val vm: SpeechRecognitionViewModel = koinViewModel()
}

2. 全局共享 VM(绑定 Activity)

整个 Activity 下所有 Compose 页面共享同一个实例。

kotlin

kotlin 复制代码
@Composable
fun SharedSpeechPage() {
    val vm: SpeechRecognitionViewModel = koinViewModel(
        viewModelStoreOwner = LocalContext.current as ViewModelStoreOwner
    )
}

3. 导航图内共享 VM(推荐规范)

只在当前导航流程内共享,退出路由自动回收,生命周期更合理。

kotlin

kotlin 复制代码
@Composable
fun NavGraphSharedPage(navBackStackEntry: NavBackStackEntry) {
    val vm: SpeechRecognitionViewModel = koinViewModel(
        viewModelStoreOwner = navBackStackEntry
    )
}

4. 最稳共享方案:CompositionLocal 提供

不依赖 Koin Compose 扩展,无版本兼容问题,100% 不崩溃。

步骤 1:Activity 创建并提供 VM

kotlin

kotlin 复制代码
class MainActivity : ComponentActivity() {
    private val sharedVm: SpeechRecognitionViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            CompositionLocalProvider(LocalSpeechVM provides sharedVm) {
                AppNavHost() // 整个导航都能共享
            }
        }
    }
}

步骤 2:定义 CompositionLocal

kotlin

go 复制代码
val LocalSpeechVM = compositionLocalOf<SpeechRecognitionViewModel> {
    error("请在 Activity 中通过 LocalSpeechVM 提供实例")
}

步骤 3:页面直接使用

kotlin

kotlin 复制代码
@Composable
fun PageA() {
    val vm = LocalSpeechVM.current
}

@Composable
fun PageB() {
    val vm = LocalSpeechVM.current // 同一实例
}

三、ViewModel 带参注入(实战高频)

1. Koin 模块定义(支持动态参数)

kotlin

ini 复制代码
viewModel { (id: Int) ->
    DetailViewModel(
        id = id,
        repo = get(),
        savedStateHandle = get()
    )
}

2. Compose 中传入参数

kotlin

kotlin 复制代码
@Composable
fun DetailPage(itemId: Int) {
    val vm: DetailViewModel = koinViewModel(
        parameters = { parametersOf(itemId) }
    )
}

kotlin

scss 复制代码
composable("detail/{id}") { backStack ->
    val id = backStack.arguments?.getInt("id") ?: 0
    val vm: DetailViewModel = koinViewModel(
        parameters = { parametersOf(id) }
    )
    DetailPage(vm)
}

四、SavedStateHandle 自动注入

Koin 可以自动注入 SavedStateHandle,无需手动处理。

1. ViewModel

kotlin

kotlin 复制代码
class SearchViewModel(
    val savedStateHandle: SavedStateHandle
) : ViewModel() {
    // 保存页面状态
    var keyword by mutableStateOf(savedStateHandle["keyword"] ?: "")
        private set

    fun updateKeyword(text: String) {
        keyword = text
        savedStateHandle["keyword"] = text
    }
}

2. Koin 注册

kotlin

scss 复制代码
viewModel { SearchViewModel(get()) }

3. Compose 使用

kotlin

kotlin 复制代码
@Composable
fun SearchPage() {
    val vm: SearchViewModel = koinViewModel()
}

下面是可直接复制运行的完整导航结构,包含:

  • 根导航
  • 嵌套导航(如 "用户中心" 模块)
  • 嵌套导航内页面共享 VM

1. 定义路由表

kotlin

kotlin 复制代码
object Route {
    const val HOME = "home"
    const val SEARCH = "search"
    const val DETAIL = "detail/{id}"

    // 嵌套导航
    const val USER_GRAPH = "user_graph"
    const val USER_PROFILE = "user_profile"
    const val USER_SETTING = "user_setting"
}

2. 嵌套导航共享的 ViewModel

kotlin

kotlin 复制代码
class UserViewModel : ViewModel() {
    val userName = mutableStateOf("张三")
}

3. Koin 注册

kotlin

ini 复制代码
val appModule = module {
    viewModel { UserViewModel() }
}

4. 根导航 + 嵌套导航实现

kotlin

scss 复制代码
@Composable
fun AppNavHost(
    navController: NavHostController = rememberNavController()
) {
    NavHost(
        navController = navController,
        startDestination = Route.HOME
    ) {
        // 首页
        composable(Route.HOME) {
            HomePage(navController)
        }

        // 搜索页
        composable(Route.SEARCH) {
            SearchPage()
        }

        // 详情页(带参)
        composable(Route.DETAIL) { backStack ->
            val id = backStack.arguments?.getInt("id") ?: 0
            val vm: DetailViewModel = koinViewModel(
                parameters = { parametersOf(id) }
            )
            DetailPage(vm)
        }

        // =============== 嵌套导航:用户模块 ===============
        navigation(
            startDestination = Route.USER_PROFILE,
            route = Route.USER_GRAPH
        ) {
            composable(Route.USER_PROFILE) { backStackEntry ->
                // 获取嵌套导航作用域的 VM → 嵌套内所有页面共享
                val userVm: UserViewModel = koinViewModel(
                    viewModelStoreOwner = backStackEntry
                )
                UserProfilePage(userVm)
            }

            composable(Route.USER_SETTING) { backStackEntry ->
                // 和上面同一个 UserViewModel 实例
                val userVm: UserViewModel = koinViewModel(
                    viewModelStoreOwner = backStackEntry
                )
                UserSettingPage(userVm)
            }
        }
    }
}

5. 页面示例

kotlin

kotlin 复制代码
@Composable
fun HomePage(nav: NavController) {
    Column {
        Text("首页")
        Button(onClick = { nav.navigate(Route.USER_GRAPH) }) {
            Text("去用户中心")
        }
    }
}

@Composable
fun UserProfilePage(vm: UserViewModel) {
    Text("用户名:${vm.userName.value}")
}

@Composable
fun UserSettingPage(vm: UserViewModel) {
    Text("设置页 - 用户名:${vm.userName.value}")
}

六、如何验证 ViewModel 是否真的共享?

唯一可靠方法:打印 hashCode /identityHashCode

  • 两个页面 hashCode 相同 = 真正共享
  • 不同 = 独立实例

kotlin

kotlin 复制代码
import android.util.Log

@Composable
fun UserProfilePage(vm: UserViewModel) {
    Log.d("VM_DEBUG", "Profile VM hash = ${vm.hashCode()}")
}

@Composable
fun UserSettingPage(vm: UserViewModel) {
    Log.d("VM_DEBUG", "Setting VM hash = ${vm.hashCode()}")
}

成功日志示例

plaintext

ini 复制代码
VM_DEBUG: Profile VM hash = 12345678
VM_DEBUG: Setting VM hash = 12345678

hashCode 完全一致 → 共享成功 ✅


七、为什么不推荐把 VM 当作参数传入 Compose?

很多人习惯这样写:

kotlin

kotlin 复制代码
@Composable
fun PageA(vm: SpeechRecognitionViewModel)

存在问题

  1. 容易触发无意义重组Compose 根据参数变化判断重组,父组件重组会导致子页面无辜重组。
  2. **参数透传地狱(Prop Drilling)**多层嵌套时需要层层传递 VM,代码混乱、耦合极高。
  3. 生命周期不清晰无法直观判断 VM 属于 Activity、NavGraph 还是局部组件。

更好的方式

  • 页面内部直接获取:koinViewModel(...)
  • 全局共享:CompositionLocal
  • 导航共享:koinViewModel(navBackStackEntry)

八、常见错误与崩溃原因(避坑重点)

1. 错误强转类型导致崩溃

kotlin

scss 复制代码
// 崩溃代码
koinViewModel(LocalViewModelStoreOwner.current as Qualifier?)

原因:Qualifier 是 Koin 命名标识,与 ViewModelStoreOwner 无关,强转必然类型异常。

2. 参数名写错(owner /viewModelStoreOwner)

kotlin

scss 复制代码
// 报错:No parameter with name 'owner'
koinViewModel(owner = xxx)

Koin 3.x 正确参数名为:viewModelStoreOwner。

3. 作用域为空导致崩溃

LocalViewModelStoreOwner.current 可能在某些场景(弹窗、嵌套导航)下为 null,直接使用会崩。更安全写法:

kotlin

ini 复制代码
val vm: SpeechRecognitionViewModel = koinViewModel(
    viewModelStoreOwner = LocalContext.current as ViewModelStoreOwner
)

4. 多次创建导致 "看起来没共享"

每个页面都直接写:

kotlin

scss 复制代码
koinViewModel()

这会创建独立实例,不是共享。


九、场景与最佳用法速查表

表格

场景 推荐写法 是否共享 特点
单个页面独立使用 koinViewModel() ❌ 最简单、无耦合
整个 Activity 全局共享 koinViewModel(LocalContext as Owner) ✅ 全局通用
业务流程内共享(下单 / 搜索) koinViewModel(navBackStackEntry) ✅ 生命周期合理,推荐
嵌套导航内共享 koinViewModel(backStackEntry) ✅ 模块化、自动回收
跨页面且兼容所有版本 CompositionLocal 提供 ✅ 最稳、不依赖 Koin 版本
页面恢复状态(旋转 / 后台) SavedStateHandle 自动注入 --- 状态不丢失
动态 ID / 参数页面 parametersOf(id) --- 带参 VM 标准用法

十、总结

  • Compose 中获取 VM 统一使用 koinViewModel() 函数形式
  • 共享 VM 核心是提升作用域:Activity / NavGraph / 嵌套导航
  • 验证共享只看 hashCode 是否一致
  • 尽量避免把 VM 作为参数传递,减少重组与耦合
  • 复杂状态保存使用 SavedStateHandle,Koin 可自动注入
  • 模块化业务优先使用嵌套导航共享 VM,生命周期最合理
  • 追求极致稳定 → 使用 CompositionLocal 共享方案
相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui