You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用launchFragmentInHiltContainer测试时,报addObserver需主线程调用错误

解决navController.setGraph()主线程异常问题

问题原因

JUnit的@Test方法默认在后台线程执行,而TestNavHostController.setGraph()内部会调用LiveData的addObserver方法,该方法必须在主线程执行,因此直接在测试方法中调用setGraph会抛出java.lang.IllegalStateException: Method addObserver must be called on the main thread异常。

解决方案

将navController.setGraph()的调用移到主线程中执行,有两种常用实现方式:

方式一:在测试方法中手动切换到主线程

使用InstrumentationRegistry.getInstrumentation().runOnMainSync()将setGraph操作包裹起来,确保在主线程执行:

@HiltAndroidTest
class SensorsTests {

    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    @Inject
    lateinit var repository: MockRepositoryImpl

    @BeforeAll
    fun beforeAll() {
        hiltRule.inject()
    }

    @Test
    fun userSensorsAreVisible() {
        val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
        
        // 切换到主线程执行setGraph
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            navController.setGraph(R.navigation.nav_graph)
        }

        launchFragmentInHiltContainer<SensorsFragment>(navHostController = navController)

        // 替换Thread.sleep,用Espresso的等待方法更可靠
        Espresso.onView(ViewMatchers.isRoot()).perform(ViewActions.waitForIdle())
    }
}

方式二:封装到launchFragmentInHiltContainer函数中

修改工具函数,让它内部处理导航图的设置,避免在测试方法中手动切换线程:

inline fun <reified T : Fragment> launchFragmentInHiltContainer(
    fragmentArgs: Bundle? = null,
    @StyleRes themeResId: Int = R.style.Theme_SensorsTracker,
    navGraphId: Int? = null,
    crossinline action: Fragment.() -> Unit = {}
) {
    val startActivityIntent = Intent.makeMainActivity(
        ComponentName(
            ApplicationProvider.getApplicationContext(),
            HiltTestActivity::class.java
        )
    ).putExtra(
        "androidx.fragment.app.testing.FragmentScenario.EmptyFragmentActivity.THEME_EXTRAS_BUNDLE_KEY",
        themeResId
    )

    ActivityScenario.launch<HiltTestActivity>(startActivityIntent).onActivity { activity ->
        // 在主线程中创建并配置NavController
        val navController = navGraphId?.let {
            TestNavHostController(activity).apply {
                setGraph(it)
            }
        }

        val fragment: Fragment = activity.supportFragmentManager.fragmentFactory.instantiate(
            Preconditions.checkNotNull(T::class.java.classLoader),
            T::class.java.name
        )

        fragment.arguments = fragmentArgs

        // 监听View生命周期,绑定NavController
        val lifecycleObserver = Observer<LifecycleOwner?> { viewLifecycleOwner ->
            if (viewLifecycleOwner != null) {
                navController?.let {
                    Navigation.setViewNavController(fragment.requireView(), it)
                }
            }
        }
        fragment.viewLifecycleOwnerLiveData.observeForever(lifecycleObserver)

        activity.supportFragmentManager
            .beginTransaction()
            .add(android.R.id.content, fragment, "")
            .commitNow()

        fragment.action()

        // 移除观察者,避免内存泄漏
        fragment.viewLifecycleOwnerLiveData.removeObserver(lifecycleObserver)
    }
}

对应的测试方法可以简化为:

@Test
fun userSensorsAreVisible() {
    launchFragmentInHiltContainer<SensorsFragment>(navGraphId = R.navigation.nav_graph)
    Espresso.onView(ViewMatchers.isRoot()).perform(ViewActions.waitForIdle())
}

额外优化建议

  • 避免使用Thread.sleep():这种方式不可靠,容易导致测试不稳定,改用Espresso的waitForIdle()或自定义IdlingResource等待界面加载完成。
  • 移除LiveData观察者:在工具函数中添加removeObserver调用,防止内存泄漏。

内容的提问来源于stack exchange,提问作者Captain Jacky

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 20:55:19