如何在使用条件导航的情况下将androidTest添加到片段?

3

我正在尝试向有条件导航文档中的示例添加测试导航

我创建了一个演示,并在GitHub上发布了它。

该演示包含3个片段:HomeFragmentProfileFragmentLoginFragment。在ProfileFragment中,存在一个条件检查,用于检查用户是否连接。如果用户未连接,则会将其发送到LoginFragment

ProfileFragment

@AndroidEntryPoint
class ProfileFragment : Fragment(R.layout.fragment_profile) {
    private val mainViewModel: MainViewModel by activityViewModels()
    private val navController by lazy { findNavController() }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val currentBackStackEntry = navController.currentBackStackEntry!!
        currentBackStackEntry.savedStateHandle.getLiveData<Boolean>(LoginFragment.LOGIN_SUCCESSFUL)
            .observe(currentBackStackEntry, { success ->
                if (!success) {
                    val startDestination = navController.graph.startDestination
                    val navOptions = NavOptions.Builder()
                        .setPopUpTo(startDestination, true)
                        .build()
                    navController.navigate(startDestination, null, navOptions)
                }
            })
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        mainViewModel.user.observe(viewLifecycleOwner, {
            if(!it){
                navController.navigate(R.id.loginFragment)
            }
        })
    }
}

如文档所述,在onCreate()方法中,我正在观察SavedStateHandle中存储的LOGIN_SUCCESSFUL值,以便在用户未登录时重定向他。

LoginFragment

@AndroidEntryPoint
class LoginFragment : Fragment() {
    companion object {
        const val LOGIN_SUCCESSFUL: String = "LOGIN_SUCCESSFUL"
    }

    private val viewModel: LoginViewModel by viewModels()
    private val navController by lazy { findNavController() }

    private lateinit var savedStateHandle: SavedStateHandle
    private lateinit var binding: FragmentLoginBinding

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_login, container, false)
        binding.viewModel = viewModel
        binding.lifecycleOwner = this
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        savedStateHandle = navController.previousBackStackEntry!!.savedStateHandle
        savedStateHandle.set(LOGIN_SUCCESSFUL, false)

        viewModel.login.observe(viewLifecycleOwner, ResultObserver {
            if (it.status == Status.ERROR) {
                Snackbar.make(requireView(), it.message ?: "Error", Snackbar.LENGTH_SHORT).show()
            } else if (it.status == Status.SUCCESS) {
                savedStateHandle.set(LOGIN_SUCCESSFUL, true)
                navController.popBackStack()
            }
        })
    }
}

测试

@Test
fun profileFragment_unauthenticated() {
    setAuthentication(false)

    val navController = TestNavHostController(ApplicationProvider.getApplicationContext())

    launchFragmentInHiltContainer<ProfileFragment> {
        navController.setViewModelStore(ViewModelStore())
        navController.setGraph(R.navigation.nav_main)
        navController.setCurrentDestination(R.id.profileFragment)
        Navigation.setViewNavController(requireView(), navController)
    }
    assertEquals(navController.currentDestination?.id, R.id.loginFragment)
}

因为我正在使用Hilt进行依赖注入,所以需要将片段附加到带有@AndroidEntryPoint注释的活动中:launchFragmentInHiltContainer
在测试期间,我创建了一个TestNavHostController实例并将其分配给片段。
问题是,在onCreate方法中访问navController时,navControllerRESUMED状态期间实例化(参见FragmentScenario),从而导致 java.lang.IllegalStateException: Fragment ProfileFragment ... does not have a NavController set
我知道可以通过向片段的viewLifecycleOwnerLiveData添加观察者来更早地使navController可用。但是,这还不够早。
那么,在这种情况下如何实现测试导航呢?
可能的解决方案:
经过几天的寻找片段测试的方法,我认为我找到了一种解决方案。我的想法是将navController附加到活动上。然而,我对结果并不完全满意,这就是为什么我没有将此解决方案发布为答案。对于任何有兴趣的人,我将我的解决方案添加到GitHub存储库中。
1个回答

1
我知道可以通过向片段的viewLifecycleOwnerLiveData添加观察者,使navController更早可用。但是,这还不够快。
实际上,您使用了错误的方式。
正如Google文档所述https://developer.android.com/guide/navigation/navigation-testing#test_navigationui_with_fragmentscenario 他们使用了launchFragmentInContainer,它有一个FragmentFactory来控制Fragment实例的实例化。
这就是您如何使用activity.supportFragmentManager.fragmentFactory创建片段的方法。
   val fragment: Fragment = activity.supportFragmentManager.fragmentFactory.instantiate(
        Preconditions.checkNotNull(T::class.java.classLoader),
        T::class.java.name
    )

activity.supportFragmentManager.fragmentFactory 替换为
 val fragmentFactory = object : FragmentFactory() {
        override fun instantiate(
            classLoader: ClassLoader,
            className: String
        ) = when (className) {
            T::class.java.name -> action()
            else -> super.instantiate(classLoader, className)
        }
    }

"所以你将拥有这样的片段创作方式"
 val fragment: Fragment = fragmentFactory.instantiate(
        Preconditions.checkNotNull(T::class.java.classLoader),
        T::class.java.name
    )


```crossinline action: Activity.() -> Unit = {}```
替换为
```crossinline action: () -> T```
删除```activity.action()```,因为在上面的FragmentFactory中已经使用了。
所以,
``` launchFragmentInHiltContainer() ```
将会变成这样:
inline fun <reified T : Fragment> launchFragmentInHiltContainer(
fragmentArgs: Bundle? = null,
@StyleRes themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme,
crossinline action: () -> T) {
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 ->

    val fragmentFactory = object : FragmentFactory() {
        override fun instantiate(
            classLoader: ClassLoader,
            className: String
        ) = when (className) {
            T::class.java.name -> action()
            else -> super.instantiate(classLoader, className)
        }
    }

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

    fragment.arguments = fragmentArgs
    activity.supportFragmentManager
        .beginTransaction()
        .add(android.R.id.content, fragment, "")
        .commitNow()
} }

现在您可以使用由谷歌提供的相同代码,并使用 Hilt 注入,如下所示
       launchFragmentInHiltContainer {
       ProfileFragment().also { fragment ->
            fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
                if (viewLifecycleOwner != null) {
                    navController.setViewModelStore(ViewModelStore())
                    navController.setGraph(R.navigation.nav_main)
                    navController.setCurrentDestination(R.id.profileFragment)
                    Navigation.setViewNavController(requireView(), navController)
                }
            }
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接