介绍Nibel:面向基于Fragment的应用的导航库,支持无缝使用Jetpack Compose

Nibel简介

介绍Nibel:面向基于Fragment的应用的导航库,支持在依赖于Fragment的Android应用中无缝使用Jetpack Compose。
我们构建Nibel时的目标是,为团队创建新功能时提供真正的Jetpack Compose体验,同时自动保持与代码库的兼容性。通过利用Kotlin符号处理器(KSP)的强大功能,Nibel提供了一种统一且类型安全的方式来在以下导航场景之间进行页面导航:

  1. Fragment → Compose
  2. Compose → Compose
  3. Compose → Fragment

Nibel支持单模块和多模块导航,特别适用于在功能模块之间进行导航,这些功能模块之间不直接依赖于彼此。

在本文中,您将了解如何在项目中开始使用Nibel,以及Jetpack Compose的常见采用场景和Nibel提供的自定义选项。

如何使用?

以下是在项目中使用Nibel开始采用Jetpack Compose的基本步骤:

  1. 声明一个屏幕。要开始使用Nibel,只需使用@UiEntry注解标记您的Compose函数。这将生成一个{ComposableName}Entry类,用于导航到此屏幕。
@UiEntry(type = ImplementationType.Fragment)
@Composable
fun FooScreen(
  navigator: NavigationController // optional param
) {
    
     ... }

对于带有参数的屏幕,只需在注解中传递您的Parcelable args类。

@UiEntry(
  type = ImplementationType.Composable,
  args = BarScreenArgs::class
)
@Composable
fun BarScreen(
  args: BarScreenArgs, // optional param
  navigator: NavigationController // optional param
) {
    
     ... }

稍后我们将详细查看ImplementationType

  1. 在Compose屏幕之间进行导航。使用NavigationController,可以在标记的Compose屏幕之间进行导航。只需使用navigateTo函数,并将生成的entry实例作为参数传递。
val args = BarScreenArgs(...)
navigator.navigateTo(BarScreenEntry.newInstance(args))
  1. 导航到Fragment。同样,可以使用NavigationController从Compose屏幕导航到旧的Fragment。
class BazFragment : Fragment() {
    
     ... }

将Fragment实例包装为FragmentEntry,并将其传递给navigateTo函数。

val fragment = BazFragment()
navigator.navigateTo(FragmentEntry(fragment))
  1. 从Fragment导航。标记的Compose屏幕对于非Compose世界来说表现得像Fragment。将生成的entry类视为Fragment,并使用transaction进行导航。
class QuxFragment : Fragment() {
    
    
  ...
  requireActivity().supportFragmentManager.commit {
    
    
    replace(android.R.id.content, FooScreenEntry.newInstance().fragment)
  }
}

多模块导航

在多模块应用中,通常会存在不直接依赖于彼此的功能模块。在这种情况下,无法直接获取另一个功能模块中生成的entry类的引用。

Nibel提供了一种简单的、类型安全的多模块导航方式,使用"目标"的概念。目标是一个简单的数据类型,用作导航意图,并位于一个单独的模块中,可供其他功能模块使用。

每个目标与一个屏幕相关联,因此在需要导航时,使用目标的实例来到达目标屏幕。

在应用中无需有一个单一的导航模块,可以有多个导航模块。然而,关键要求是目标类型对源屏幕和目标屏幕都可用。

  1. 声明一个目标。最基本的目标是实现DestinationWithNoArgs的对象,并在一个单独的导航模块中声明,供其他功能模块使用。
// :navigation模块
object FooScreenDestination : DestinationWithNoArgs

如果需要带有参数的屏幕,请继承DestinationWithArgs

// :feature模块,依赖于:navigation模块
data class BarScreenDestination(
  override val args: BarScreenArgs // Parcelable args
) : DestinationWithArgs<BarScreenArgs>
  1. 将目标与屏幕关联。每个目标应该使用@UiExternalEntry注解与一个屏幕相关联,放在Compose函数上。
@UiExternalEntry(
  type = ImplementationType.Fragment,
  destination = BarScreenDestination::class
)
@Composable
fun BarScreen(
  args: BarScreenArgs, // optional param
  navigator: NavigationController // optional param
) {
    
     ... }

如果需要从Compose导航到旧的Fragment,则应将@LegacyExternalEntry应用于Fragment。

@LegacyExternalEntry(destination = BasScreenDestination::class)
class BazScreenFragment : Fragment() {
    
     ... }
  1. 导航到目标。使用NavigationController导航到目标。
val args = BarScreenArgs(...)
navigator.navigateTo(BarScreenDestination(args))

@UiScreenEntry包含@UiEntry的所有功能,因此,如果您直接引用了生成的entry类,以下代码也适用于同一屏幕。

val args = BarScreenArgs(...)
navigator.navigateTo(BarScreenEntry.newInstance(args))
  1. 从Fragment导航。最后,如果需要从旧的Fragment导航到Compose屏幕,应使用目标来获取Fragment实例并执行事务。
class QuxScreenFragment : Fragment() {
    
    
  ...
  requireActivity().supportFragmentManager.commit {
    
    
    val entry = Nibel.newFragmentEntry(BarScreenDestination)!!
    replace(android.R.id.content, entry.fragment)
  }
}

Compose函数参数

使用@UiEntry@UiExternalEntry注解的Compose函数可以有任意数量的参数,只要它们有默认值。

@UiEntry(type = ImplementationType.Fragment)
fun FooScreen(viewModel: FooViewModel = viewModel()) {
    
     ... }

此外,还有一些特殊类型的参数不需要默认值,因为Nibel可以找出如何提供相应的实例。这些特殊类型的参数包括NavigationController、ImplementationType,以及与@UiEntry注解或目标类型中相同类型的args。

@UiEntry(
  type = ImplementationType.Composable, 
  args = BarArgs::class
)
fun BarScreen(
  args: BarArgs,
  navigator: NavigationController,
  type: ImplementationType
) {
    
     ... }

如果参数类型不匹配,将会抛出编译时错误。

另外,上述值也可以作为组合局部变量获取。

@UiEntry(
  type = ImplementationType.Composable, 
  args = BarArgs::class
)
fun BarScreen() {
    
    
  val args = LocalArgs.current as BarArgs
  val navigator = LocalNavigationController.current
  val type = LocalImplementationType.current
}

常见用法场景

根据目标屏幕的注解中指定的ImplementationType,生成的条目类型有所不同。每种类型都适用于特定的场景,可能是以下之一:

  1. Fragment - 生成的条目是一个使用带注解的Compose函数作为其内容的Fragment。它使得Compose屏幕对其他Fragment可见,对于fragment → compose导航场景至关重要。
  2. Composable - 生成一个小的包装类覆盖Compose函数。通常在compose → composecompose → fragment导航场景中使用,后者使用这种实现类型进行标记。

通常,将新的Jetpack Compose屏幕添加到现有代码库有几种常见场景。

场景1 - 新功能

最简单的情况是在单独的模块中创建全新的功能。在这种情况下,该功能的所有屏幕都使用Jetpack Compose。

功能的第一个屏幕作为外部入口,允许从其他模块进入该功能。它必须标记为ImplementationType.Fragment。这将确保它在非Compose代码中显示为一个Fragment,因此,轻松地从旧Fragment导航到这个新功能。

功能中的所有后续屏幕应该使用ImplementationType.Composable。这将提高性能,因为不会生成Fragment,从而导致每个屏幕的类分配更少。

在某些情况下,您可能需要从新功能返回到依赖于Fragment的旧功能。您只需要用@LegacyExternalEntry注解目标Fragment,并通过NavigationController使用其关联的目标进行导航。

场景2 - 扩展现有功能

另一种情况是需要在现有功能中插入一系列新的连续屏幕。在这种情况下,即使这些屏幕位于Fragment流程的中间,它们也可以是Compose屏幕。

关键规则仍然相同。第一个Compose屏幕应该标记为ImplementationType.Fragment,而所有后续屏幕应该使用ImplementationType.Composable

场景3 - 独立屏幕

第三种情况可能在采用Jetpack Compose的早期阶段最常见。在这种情况下,独立的Compose屏幕放置在旧Fragment流程的中间。

在这种情况下,您只需要用ImplementationType.Fragment注解Compose屏幕,并在Compose代码之外将它们视为Fragment。

自定义设置

Nibel提供了各种自定义选项,以便根据特定项目的需求进行适应。

在继续本节之前,建议查阅我们之前的故事,其中更详细地介绍了Nibel的内部组件以及其背后实现的思路。

应用主题

对于每个用ImplementationType.Fragment注解的屏幕,Nibel会生成一个entry类,它是一个继承自ComposableFragment的fragment。在使用Jetpack Compose与fragments时,所有的可组合UI都设置在onCreateView中。这意味着对于每个新的fragment,都必须显式地应用一个主题。

// 生成的fragment的基类
// (位于nibel-runtime库的一部分)
abstract class ComposableFragment : Fragment() {
    
    

  @Composable
  abstract fun ComposableContent()

  override fun onCreateView(
    ...
  ) = ComposeView(requireContext()).apply {
    
    
    setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
    setContent {
    
    
      AppTheme {
    
     // 在这里应用主题
        ...
      }
    }
  }
}

由于这是第三方库的基类,无法直接应用特定应用的自定义主题。

要应用主题,您可以实现一个RootDelegate,这将是一个只有一个简单的组合函数的类。这个函数会在每个用ImplementationType.Fragment注解的屏幕的根部调用。

object CustomRootContent : RootDelegate {
    
    

  @Composable
  override fun Content(content: @Composable () -> Unit) {
    
    
    AppTheme {
    
     // 应用自定义主题
      content()
    }
  }
}

不要忘记调用content函数,以继续UI的构建。

然后在配置Nibel时,只需应用CustomRootContent

Nibel.configure(rootDelegate = CustomRootContent)

导航规范

正如上面所述,NavigationController用于在屏幕之间进行导航。然而,navigateTo函数也提供了自定义的空间。

abstract class NavigationController(...) {
    
    
  ...

  fun navigateTo(
    entry: Entry,
    fragmentSpec: FragmentSpec<*>, // fragment导航规范
    composeSpec: ComposeSpec<*> // compose导航规范
  )
}

正如我们已经知道的,Nibel允许在各种情况下在fragment和compose屏幕之间导航。当从可组合函数导航到新屏幕时,Nibel在内部使用不同的工具进行导航,具体取决于目标屏幕。

如果下一个屏幕是一个fragment或者用ImplementationType.Fragment注解的可组合函数,将自动执行一个fragment事务。
如果下一个屏幕是用ImplementationType.Composable注解的可组合函数,Nibel将隐式地使用compose导航库进行导航。
在任何时候,您都可以在注解中切换ImplementationType,代码仍然可以编译。然而,Nibel将使用不同的底层工具进行导航。FragmentSpec用于在底层进行fragment之间的导航,而ComposeSpec用于直接导航可组合函数之间的导航,也是隐式的。

在进行底层fragment事务时,有时您可能希望更多地控制其细节。例如,使用add vs replace,选择自定义容器id进行事务等等。

每个导航规范的实例都包含如何执行导航的实现细节。因此,可以通过多种方式进行自定义。

例如,在fragment事务中,您可以使用FragmentTransactionSpec的实例,并指定事务的详细信息。

navigator.navigateTo(
  entry = ..., 
  fragmentSpec = FragmentTransactionSpec(
    replace = true,
    addToBackStack = true,
    containerId = R.id.customContainerId
  )
)

如果这还不够,您可以通过编写自定义导航逻辑来完全覆盖其行为。

class CustomTransactionSpec : FragmentTransactionSpec(...) {
    
    

  override fun FragmentTransactionContext.navigateTo(entry: FragmentEntry) {
    
    
    this.fragmentManager.commit {
    
    
      // 自定义fragment事务逻辑
    }
  }
}

然后通过使用CustomTransactionSpec进行导航。

navigator.navigateTo(
  entry = ..., 
  fragmentSpec = CustomTransactionSpec()
)

对于compose规范,底层使用了compose导航库。所有导航目标都动态添加到ComposeNavigationSpec中。您可以在我们之前的帖子中了解有关底层如何使用compose导航库的更多信息。

最后,在配置Nibel时,可以为应用中的所有屏幕设置任何导航规范。

Nibel.configure(
  fragmentSpec = CustomFragmentSpec(),
  composeSPec = CustomComposeSpec()
)

与架构组件的兼容性

现代Android应用使用各种架构组件,例如Hilt、ViewModel等。让我们看看如何将Nibel与它们集成。

您可以将ViewModel声明为可组合函数的参数,并使用hiltViewModel来获取其实例。现在,您可以在一个由Hilt注入的ViewModel和Nibel屏幕的组合中使用它。

@UiEntry(type = Composable, args = FooArgs::class)
@Composable
fun FooScreen(viewModel: FooViewModel = hiltViewModel()) {
    
     ... }
@HiltViewModel
class FooViewModel(handle: SavedStateHandle): ViewModel() {
    
    
  val args = handle.getNibelArgs<FooArgs>()
}

您可以注意到,屏幕参数自动在ViewModel的SavedStateHandle中可用。

结论

有了Nibel,您可以专注于使用Jetpack Compose为应用程序编写新的产品功能,同时处理代码库的兼容性,尤其是处理fragment方面的问题。

Nibel具有高度可定制性,因此可以应用于各种类型的项目和在采用Jetpack Compose时的导航场景中。

Github

https://github.com/open-turo/nibel/tree/main/sample
https://github.com/open-turo/nibel

猜你喜欢

转载自blog.csdn.net/u011897062/article/details/132077233
今日推荐