美文网首页
Android导航研究案例

Android导航研究案例

作者: BlueSocks | 来源:发表于2022-11-07 22:21 被阅读0次

    本文旨在寻找一种更好的方法来建立不同功能模块之间的导航,使模块之间的耦合度低,易于维护和测试。

    导航框架

    示例考虑以下情况:

    所有的功能模块和组合根,使用jetpack导航,基本上每个模块都有自己的导航图,上面声明了所有的片段、对话……然后我们把所有的特征导航文件都包含到主导航文件中,主导航文件位于组合根目录(应用模块)中。

    然后,jetpack导航将仅作为导航路径和处理每个导航事务的真实来源。

    <?xml version="1.0" encoding="utf-8"?>  
    <navigation xmlns:android="<http://schemas.android.com/apk/res/android">  
        xmlns:app="<http://schemas.android.com/apk/res-auto">  
        android:id="@+id/app_navigation"  
        app:startDestination="@id/home_navigation">  
    
        <include app:graph="@navigation/home_navigation"/>  
    
        <include app:graph="@navigation/restaurant_navigation"/>  
    
        <include app:graph="@navigation/checkout_navigation"/>  
    
    </navigation>
    

    我相信所有的选项都可以在没有jetpack导航的情况下实现,所以如果我们想改变导航系统,我们所要做的就是在界面实现上替换导航。我们在Scalable Navigation文章中有一个类似的组合根的方法,请查看参考资料以了解更多细节,但是此人向我们展示了一个使用FragmentManager而不是jetpack导航的更细粒度的实现。

    使用jetpack导航的一个很好的理由是合成导航是基于它的,所以当我们尝试它时,它将更容易支持。

    内部导航

    对于内部导航,仅考虑使用组合根的选项,我认为我们仍然会在复合根层上添加委托的实现,因为我们不知道在功能模块内部应该去哪里。

    因此,如果你有一个屏幕a,可以导航到屏幕B(同一模块的一部分)和屏幕C(另一个模块的一部分),我们需要使用ISP将代表分成内部和外部,这有意义吗?欢迎讨论,但我宁愿有一个单一的界面和一切遵循相同的模式。

    测试解决方案

    深层联系/意图;
    源模块知道目标模块;
    源模块知道目标模块的组成根;
    组成根与授权;
    深层链接

    我不喜欢这个解决方案,因为我们需要使用URI进行导航,这意味着源特性需要知道目标特性的详细信息,例如,特性A需要知道特性B的URI才能导航到它。

    我们可以在导航模块上使用深层链接,这会导致缓存失效的问题,因为所有的URI都在这里。或者每个特性在公共模块中都有自己的URI。

    通常情况下,人们倾向于在多个活动中使用这种方法或意向式导航,理想情况是活动更少,每个模块只有片段。这种方法的实现与其他方法没有太大区别,基本上在最后我们只需要将一个URI传递给navController,所以我们可以使用feature module选项的组合来实现它。但上述问题依然存在。

    Source knows target

    对于这种方法,每个模块将声明一个导航接口,如:

    interface ICheckoutNavigator {  
        fun openCheckout(args: CheckoutArgs, context: Fragment)  
    }
    

    在public模块上,所有想要导航到checkout特性的模块都需要将:checkout:public模块声明为依赖项才能访问此接口或导航所需的任何参数。

    这个接口的实现将驻留在checkout impl模块中。

    class CheckoutNavigator : ICheckoutNavigator {  
    
        override fun openCheckout(args: CheckoutArgs, context: Fragment) {  
            context.findNavController().navigate(R.id.checkout_navigation, bundleOf("args" to args))  
        }  
    }
    

    我发现这种方法存在一些问题:

    我发现这种方法有一些问题:我们将有关签出的详细信息公开给使用此接口的任何其他功能,例如,如果我们有更多屏幕,源功能模块将知道签出包含的所有屏幕。

    所有的屏幕都需要知道导航规则,所以我们将这些屏幕与导航或应用程序的详细信息结合起来,每个功能模块/屏幕都应该足够独立于系统独立运行。基本上,每一个屏幕都应该有她触发的某种事件,并且知道导航规则的人处理该事件并导航。

    对于这种方法,我们还可以有一个导航模块,在这个模块中,我们为每个模块定义参数和导航接口,但是我们最终会遇到相同的问题,即featurex会使整个模块的缓存失效,可能还有其他依赖项。这可能适用于小型项目,但一旦项目开始增长,您就需要开始考虑其他解决方案。

    Source knows target with composition root

    这种方法与前一种方法相同,但不是在每个功能模块上实现导航接口,而是在组合根(应用程序模块)内部。

    一般情况下,复合根用户知道每个特性,那么她可能是接收来自屏幕的事件的层,但是,我们仍然存在屏幕知道导航细节以及依赖于导航的每个屏幕的问题。

    带委托的composition根

    在我看来,这是我们应该进一步探索和尝试采用的解决办法。它是一种控制反转,但有导航组件。

    基本上,每个屏幕都将声明一个委托,其中包含一个事件契约,组合根层将监听这些事件并导航到其他屏幕。

    interface IRestaurantCatalogDelegate {  
        fun checkoutButtonClick(args: CheckoutArgs)  
    
        fun itemDetailsClick()  
    }  
    
    class RestaurantCatalogFragment : Fragment {
        private val delegate by inject<IRestaurantCatalogDelegate> { parametersOf(activity) }
    // ...
    }
    

    有了这个合同,我们可以避免视图的责任,知道她需要去哪里,或任何其他功能的细节。因为她所要做的就是调用一个方法,其他人将负责导航或任何相关的逻辑。也可以使用带有密封类的事件库来实现此委托,例如:

    sealed class RestaurantCatalogEvents {  
        data class CheckoutClick(args: CheckoutArgs) : RestaurantCatalogEvents()  
        object DetailsClick : RestaurantCatalogEvents()  
    }
    

    委托将有一个方法接收事件作为参数,这取决于事件的数量这种方法可能是一个问题,有一个when与许多情况。每个事件只有一个方法,我们可以使用ISP将接口拆分成多个。

    这种方法的另一个好处是,如果您的项目有一个演示应用程序(一个可以单独运行特性的模块),您可以出于测试目的更改导航的行为,而无需更改功能模块中的任何代码

    在构图的根上,我们要做的就是:

    class RestaurantCatalogDelegate(private val activity: AppCompatActivity) :  
        INavigatorProvider by DefaultNavigatorProvider(  
            activity  
        ), IRestaurantCatalogDelegate {  
    
        override fun checkoutClick(args: CheckoutArgs) {  
            controller.navigate(checkout.id.checkout_navigation, bundleOf("args" to args))  
        }  
    
        override fun itemDetailsClick() {  
            controller.navigate(restaurant.id.itemDetailFragment)  
        }  
    }
    

    导航参数

    这里我们有一个注意点,就是CheckoutArgs对象正在餐厅特性中使用,将一个特性耦合到另一个特性,这个对象存在于checkout特性的公共模块中。

    我们可以做些什么来避免这个问题,例如,创建某种映射器,它将在导航到签出屏幕之前在合成根目录上执行。所以我们会有一些类似的东西:

    // Restaurant module
    data class RestaurantCatalogDTO(val items: Int)
    
    // Composition root
    ...
    override fun checkoutButtonClick(args: RestaurantCatalogDTO) {
        val checkoutArgs = RestaurantMapper.dtoToCheckoutArgs(args)
        controller.navigate(checkout.id.checkout_navigation, bundleOf("args" to checkoutArgs))  
    }
    ...
    

    这样做,我们就可以避免在餐厅功能模块中存在结帐的公共依赖性。

    问题是,在餐厅内结账时使用args对项目/架构有害吗?而且,我们是应该对委托的实现进行解析,还是应该在导航之前找到一个更合适的层呢。我们还有另一个选择,那就是停止使用复杂的对象在屏幕之间导航,并使用语言类型,如餐厅ID(字符串)、项目ID(Int)…这样我们可以更好地支持深层链接,因为通常我们只有东西的ID来加载屏幕。

    但是,这带来了一些问题,比如:

    增加API的负载以检索对象;
    有某种本地缓存,所以您可以在屏幕之间检索它,并处理缓存策略。。。

    撰写

    如果您希望将jetpack compose与composition根方法一起使用,我们可以轻松地创建一个支持组合布局的活动,或者在现有项目中,只创建一个片段,它将为视图返回一个可组合的函数。

    override fun onCreateView(  
        inflater: LayoutInflater,  
        container: ViewGroup?,  
        savedInstanceState: Bundle?  
    ) = setContent {  
        BuildOrderTrackerScreen()  
    }  
    
    @Preview  
    @Composable  
    fun BuildOrderTrackerScreen() {  
        Column(  
            modifier = Modifier  
                .fillMaxSize(),  
            Arrangement.Center,  
            Alignment.CenterHorizontally,  
        ) {  
            Text("Hello from Compose")  
        }  
    }
    

    其余的都是一样的,创建jetpack导航图并在其中包含这个片段,我们要导航到它只需调用导航,就像我们在其他示例中所做的那样。

    如果您决定使用compose构建整个特性,我相信我们可以使用这个片段作为整个特性的主干,不需要为每个屏幕创建一个新的片段,因为compose不需要片段。

    相关文章

      网友评论

          本文标题:Android导航研究案例

          本文链接:https://www.haomeiwen.com/subject/slvitdtx.html