Android导航研究案例

2022-11-07  本文已影响0人  BlueSocks

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

导航框架

示例考虑以下情况:

所有的功能模块和组合根,使用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不需要片段。

上一篇下一篇

猜你喜欢

热点阅读