介绍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 → compose
和compose → 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
狂奔中的毛豆: 为什么在HSP中,preferences没有被初始化,其原因没有讲出来。看出来还是一笔糊涂账
moneyxjj: rgb文件格式是什么
Coder何小二: 还是苹果好,闭环控制,只要app安全就行
随缘的人_: 你这种同步 是下载全部吗?我只想下载某个版本怎么做,
没有明天的家伙: 这里好像都是调用adb命令读取的,那普通应用要怎么样才能读取到其他应用的cpu使用率呢。/proc文件系统在hidepid不等于0的情况下普通应用是无法访问/proc/[pid]目录下的文件的,只能访问自己pid的。请问大佬有解决办法吗?