" An app only needs an Activity, you can use Fragments, just don't use the backstack with fragments "
<br/> -- Jake Wharton @Droidcon NYC 2017
近年来,SPA,即单Activity架构逐渐开始受到欢迎,随之而生了很多优秀的三方库,大部分是基于Fragment作为实现方案,其中最有代表性的就是Fragmentation了,后来Jetpack Navigation的诞生也标志着Google从官方立场对SPA架构的肯定。
Navigation的出现并没有加速Fragment对Activity的全面取代,一个重要原因是其过于依赖配置(NavGraph),丧失了Activity的灵活性。这一点上Fragmentation做的不错,有接近Activity的使用体验,可惜其不支持Kotlin,且早已停止维护,无法使用近年来在AndroidX中引入的各种新特性。
是否有一个工具,既具备Fragmentation那样灵活性,又能像Navigation那样兼容AndroidX
中的新功能呢?
Fragivity正是在这个背景下诞生的:https://github.com/vitaviva/fragivity
<br/>
Fragivity : Use Fragment like Activity
顾名思义,Fragivity希望让Fragment具备Activity一样的使用体验,从而在各种场景中能真正取而代之:
- 生命周期与Activity行为一致
- 支持多种LaunchMode
- 支持OnBackPressed事件处理、支持SwipeBack
- 支持Transition、SharedElement等转场动画
- 支持以Dialog样式显示
- 支持Deep Links
Fragivity底层基于Navigation实现,同时兼具Fragmentation的灵活性,无需配置NavGraph即可实现画面跳转。简单对比一下三者的区别:
Fragmentation | Navigation | Fragivity | |
---|---|---|---|
自由跳转 | yes | no (依赖NavGraph) | yes |
Launch Mode | 3种 | 2种 | 3种 |
支持Deep Links | no | yes(依赖NavGraph) | yes(使用注解) |
kotlin友好 | no | yes | yes |
生命周期 | 与Activity不一致(add方式) | 与Activity不一致(replace方式) | 与Activity一致 |
Fragment间通信 | startFragmentForResult | viewmodel | viewmodel、callback、resultapi等多种方式 |
过场动画 | View Animation | Transition Animation | Transition Animation |
Swipe Back | yes(依赖基类) | no | yes (无需基类) |
支持Dialog显示 | no | yes | yes |
OnBackPressed拦截 | yes (依赖基类) | yes(无需基类) | yes(无需基类) |
通过对比可以发现,比起前两者Fragivity在多个维度上与Activity的行为更加一致。
<br/>
1. 基本使用
Fragivity的接入成本很低。
1.1 gradle依赖
implementation 'com.github.fragivity:core:$latest_version'
1.2 声明NavHostFragment
像Navigation一样,Fragivity需要NavHostFragment
作为Parent,然后在ChildFragment
之间实现页面跳转。
我们在xml中声明NavHostFragment
frameLabelStart--frameLabelEnd
1.3 加载首页
通常我们需要定义一个MainActivity作为入口,同样,这里通过loadRoot
加载一个初始的Fragment:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host) as NavHostFragment
navHostFragment.loadRoot(HomeFragment::class)
}
}
1.4 页面跳转
接下来便可以在Fragment之间进行跳转了
//跳转到目标Fragment
navigator.push(DestinationFragment::class)
//携带参数跳转
val bundle = bundleOf(KEY_ARGUMENT to "some args")
navigator.push(DestinationFragment::class, bundle)
//可以通过尾lambda设置参数,好处是还可以进行其他配置
navigator.push(DestinationFragment::class) {
arguments = bundle //设置参数
launchMade = ... //更多其他设置
}
1.5 页面返回
通过pop
方法可以返回上一页面
//返回上一页面
navigator.pop()
//返回到指定页面
navigator.popTo(HomeFramgent::class)
1.6 转场动画
基于Navigation的能力,在画面跳转时可以设置Transition
动画
navigator.push(UserProfile::class, bundle) { //this:NavOptions
//配置动画
enterAnim = R.anim.enter_anim
exitAnim = R.anim.exit_anim
popEnterAnim = R.anim.enter_anim
popExitAnim = R.anim.exit_anim
}
借助FragmentNavigatorExtras
还可以设置SharedElement
,实现更优雅地动画效果
//跳转时,对imageView设置SharedElement
navigator.push(UserProfile::class, bundle) { //this:NavOptions
enterAnim = R.anim.enter_anim
exitAnim = R.anim.exit_anim
popEnterAnim = R.anim.enter_anim
popExitAnim = R.anim.exit_anim
//配置共享元素
sharedElements = sharedElementsOf(imageView to "iv_id")
}
class UserProfile : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//目标Fragment中设置共享元素动画
sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
}
}
<br/>
2. 无需配置实现页面跳转
Navigation需要配置NavGraph
才能实现页面间跳转,例如:
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
app:startDestination="@+id/first">
<fragment
android:id="@+id/fragment_first"
android:name=".FirstFagment"
android:label="@string/tag_first">
<action
android:id="@+id/action_to_second"
app:destination="@id/fragment_second"/>
</fragment>
<fragment
android:id="@+id/fragment_second"
android:name=".SecondFragment"
android:label="@string/tag_second"/>
</navigation>
每个<navigation/>
对应一个NavGraph
对象,<fragment/>
会对应到NavGraph中的各个Destination
,NavController
持有NavGraph通过控制Destination之间的跳转。
依赖配置的页面跳转,无法做到像Activity那样灵活。Fragivity通过动态构建NavGraph,无需配置即可实现跳转:
2.1 动态创建Graph
加载首页时,动态创建Graph
fun NavHostFragment.loadRoot(root: KClass<out Fragment>) {
navController.apply {
//添加Navigator
navigatorProvider.addNavigator(
FragivityNavigator(
context,
childFragmentManager,
id
)
)
//创建Graph
graph = createGraph(startDestination = startDestId) {
val startDestId = root.hashCode()
//添加startDestination
destination(
FragmentNavigatorDestinationBuilder(
provider[FragivityNavigator::class],
startDestId,
root
))
}
}
}
FragivityNavigator
负责处理页面跳转的逻辑,后文会单独介绍。Graph创建后添加startDestination
用来加载首页。
2.2 动态添加Destination
除startDestination以外,每当跳转到新页面,都需要为Graph动态添加此Destination:
fun NavHost.push(
clazz: KClass<out Fragment>,
args: Bundle? = null,
extras: Navigator.Extras? = null,
optionsBuilder: NavOptions.() -> Unit = {}
) = with(navController) {
// 动态创建Destination
val node = putFragment(clazz)
// 调用NavController的navigate方法进行跳转
navigate(
node.id, args,
convertNavOptions(clazz, NavOptions().apply(optionsBuilder)),
extras
)
}
// 创建并添加Destination
private fun NavController.putFragment(clazz: KClass<out Fragment>): FragmentNavigator.Destination {
val destId = clazz.hashCode()
lateinit var destination: FragmentNavigator.Destination
if (graph.findNode(destId) == null) {
destination = (FragmentNavigatorDestinationBuilder(
navigatorProvider[FragivityNavigator::class],
destId,
clazz
)).build()
graph.plusAssign(destination)// 添加进Graph
} else {
destination = graph.findNode(destId) as FragmentNavigator.Destination
}
return destination
}
创建Destination后,通过NavController的navigate
方法跳转到此Destination。
<br/>
3. BackStack及生命周期
如J神所说,Fragment无法很好替代Activity的原因之一是在BackStack管理上的差异,这会影响到生命周期的不同。
设想以下场景: A页面 > (启动)> B页面 >(返回)> A页面
我们知道添加Fragment一般有两种方式:add
、 replace
。无论哪种方式其在画面跳转时的生命周期与Activity都不相同:
页面B的启动方式 | 从B返回时的生命周期 |
---|---|
Activity | ActivityB:onPasue -> onStop -> onDestroy <br/> ActivityA:onStart -> onResume |
Fragment(add ) | FragmentB : onPause -> onStop -> onDestroy <br/> FragmentA : no change |
Fragment(replace) | FragmentB: onPause -> onStop -> onDestroy <br/> FragmentA: onCreateView -> onStart -> onResume |
如果希望在画面跳转时Fragment的生命周期与Activity行为一致,则至少需要达成以下三个目标:
- 目标1:回退时,FragmentB不重新onCreateView (add方式满足)
- 目标2:回退时,FragmentB会触发onStart -> onResume (replace方式满足)
- 目标3:后台的Fragment不跟随父生命周期发生变化 (replace方式满足)
无论Navigation还是Fragmentation都不能同时满足上面三条。
3.1 重写FragmentNavigator
NavController通过FragmentNavigator实现具体的跳转逻辑,FragmentNavigator是Navigator的派生类,专门负责FragmentNavigator.Destination类型的跳转。
navigate()
实现了Fragment跳转的具体逻辑,其核心代码如下
@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> {
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
String className = destination.getClassName();
//实例化Fragment
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();
ft.replace(mContainerId, frag); // replace方式添加Fragment
ft.setPrimaryNavigationFragment(frag);
//事务压栈
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
ft.setReorderingAllowed(true);
ft.commit();
}
}
FragmentNavigator通过replace进行Fragment跳转,前面分析我们知道这在回退时会重新onCreateView
,不符合预期。我们实现子类FragivityNavigator
,重写navigate()
方法,将replace改为add,避免重新onCreateView,达成“目标1”
public class FragivityNavigator extends FragmentNavigator {
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
//ft.replace(mContainerId, frag); // replace改为add
ft.add(mContainerId, frag, generateBackStackName(mBackStack.size(), destination.getId()));
}
}
3.2 添加OnBackStackChangedListener
在合适的时机为FragmentManger添加OnBackStackChangedListener
,当监听到backstack变化时,手动触发生命周期回调,达成“目标2”
private final FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener =
new FragmentManager.OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
if (mIsPendingAddToBackStackOperation) {
mIsPendingAddToBackStackOperation = !isBackStackEqual();
if (mFragmentManager.getFragments().size() > 1) {
// 切到后台时的生命周期
Fragment fragment = mFragmentManager.getFragments().get(mFragmentManager.getFragments().size() - 2);
if (fragment instanceof ReportFragment) {
fragment.performPause();
fragment.performStop();
((ReportFragment) fragment).setShow(false);
}
}
} else if (mIsPendingPopBackStackOperation) {
mIsPendingPopBackStackOperation = !isBackStackEqual();
// 回到前台时的生命周期
Fragment fragment = mFragmentManager.getPrimaryNavigationFragment();
if (fragment instanceof ReportFragment) {
((ReportFragment) fragment).setShow(true);
fragment.performStart();
fragment.performResume();
}
}
}
};
3.3 ReportFragment代理
为了达成“目标3”, 在实例化Fragment时,为其创建ReportFragment
作为代理。所谓代理其实是通过ParentFragment
对内进行生命周期的分发和控制:
//ReportFragment
internal class ReportFragment : Fragment() {
internal lateinit var className: String
private val _real: Class<out Fragment> by lazy {
Class.forName(className) as Class<out Fragment>
}
private val _realFragment by lazy { _real.newInstance() }
override fun onAttach(context: Context) {
super.onAttach(context)
//将目标Framgent作为child进行管理
mChildFragmentManager.beginTransaction().apply {
_realFragment.arguments = arguments
add(R.id.container, _realFragment)
commitNow()
}
}
}
//ReportFragmentManager
internal class ReportFragmentManager : FragmentManager() {
//isShow:在后台时,不响应生命周期分发
internal var isShow = true
public override fun dispatchResume() {
if (isShow) super.dispatchResume()
}
//...
}
<br/>
4. 支持Launch Modes
Fragivity支持三种Launch Modes:Standard
、SingleTop
、SingleTask
。
启动方式非常简单:
navigator.push(LaunchModeFragment::class, bundle) { //this: NavOptions
launchMode = LaunchMode.STANDARD // 默认可省略
//or LaunchMode.SINGLE_TOP, LaunchMode.SINGLE_TASK
}
这里着重介绍一下SingleTop的实现。Navigation也支持SingleTop,但是在Navigator
中完成的,由于我们重写了Navigator(replace改为add),因此对SingleTop的实现也要做相应调整:
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
final Fragment preFrag = mFragmentManager.getPrimaryNavigationFragment();
//当以singleTop启动时
if (isSingleTopReplacement) {
if (mBackStack.size() > 1) {
ft.remove(preFrag);// 删除旧实例
//更新FragmentTransaction中的实例信息
frag.mTag = generateBackStackName(mBackStack.size() - 1, destination.getId());
if (mFragmentManager.mBackStack.size() > 0) {
List<FragmentTransaction.Op> ops =
mFragmentManager.mBackStack.get(mFragmentManager.mBackStack.size() - 1).mOps;
for (FragmentTransaction.Op op : ops) {
if (op.mCmd == OP_ADD && op.mFragment == preFrag) {
op.mFragment = frag;
}
}
}
}
}
}
SingleTop要求当栈顶类型和目标类型相同时只能存在一个实例,所以需要删除旧实例避免重复添加。同时为了保证BackStack回退时的事务行为正常,需要将添加旧实例的事务中的相关信息更新为新实例。
<br/>
5. Fragment间通信
Fragivity支持androidx.fragment
的所有通信方式,例如使用ViewModel,或者使用ResultApi(Fragment 版本高于1.3.0-beta02
)等。除此之外,Fragivity提供了更简单的基于Callback的通信方式:
//SourceFragment
val cb = { it : Boolean ->
//...
}
navigator.push {
DestinationFragment(cb)
}
//Destination
class DestinationFragment(val cb:(Boolean) -> Unit) {...}
以前Fragment如果必须使用无参的构造函数,否则打包时会出错。感谢AndroidX带来的进步,目前已经取消了此限制,允许自定义带参数的构造函数。因此我们可以通过lambda动态创建Fragment并将callback作为构造参数传入。
inline fun <reified T : Fragment> NavHost.push(
noinline optionsBuilder: NavOptions.() -> Unit = {},
noinline block: () -> T
) {
//...
push(T::class, optionsBuilder)
}
如上,其内部仍然是使用Fragment的Class作为参数进行跳转,只是借助kotlin的reified
特性,获取了泛型的Class信息而已。
<br/>
6. 支持Deep Links
Activity可以通过URI
隐式启动,为了覆盖此类使用场景,需要为Fragment提供Deep Links
支持。Navigation在NavGraph中为Destination配置URI信息;Fragivity虽然没有NavGraph,但可以通过注解配置URI。基本思想类似于ARouter的路由原理:
- 在编译期通过kapt解析注解,获取URI信息,并与Fragment相关联
- 在Activity的入口处拦截Intent,解析URI并跳转到相关联的Fragment
6.1 添加kapt依赖
kapt 'com.github.fragivity:processor:$latest_version'
6.2 配置URI
定义Fragment时,使用@DeepLink
配置URI
const val URI = "myapp://fragitiy.github.com/"
@DeepLink(uri = URI)
class DeepLinkFragment : AbsBaseFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_deep_link, container, false)
}
}
6.3 处理Intent
在MainActivity入口处,处理Intent中的URI
//MainActivity#onCreate
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host) as NavHostFragment
navHostFragment.handleDeepLink(intent)
}
handleDeepLink
内部最终会调用NavController的相关方法对URI进行解析:
//NavController
public void navigate(@NonNull Uri deepLink) {
navigate(new NavDeepLinkRequest(deepLink, null, null));
}
之后,我们就可以从APP外部通过URI的方式跳转到目标Fragment了:
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("myapp://fragitiy.github.com/"))
startActivity(intent)
<br/>
7. OnBackPressed事件拦截
Fragment没有Activity的OnBackPressed
方法,Fragmentation通过继承的方式增加了onBackPressedSupport
方法,但这会引入新的基类,对业务代码的侵入性较高。
Fragivity基于androidx.activity
的OnBackPressedDispatcher
,以更加无侵的方式拦截back键事件。OnBackPressedDispatcher通过责任链模式保证了back事件消费的顺序,同时感知Lifecycle
,在适当的时机自动注销,避免泄露。
参考:https://developer.android.com/guide/navigation/navigation-custom-back
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback( this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// 拦截back键事件
}
})
}
back键返回与pop()返回
Fragivity提供pop
方法,通过代码实现返回,其内部最终会调用Navigator#popBackStack
。为了保证回退逻辑统一,我们希望back键回退时也由popBackStack统一处理。Navigation通过NavHostFragment进行了实现:
//NavHostFragment#onCreate
public void onCreate(@Nullable Bundle savedInstanceState) {
//...
mNavController = new NavHostController(context);
mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
//...
}
//NavController#setOnBackPressedDispatcher
void setOnBackPressedDispatcher(@NonNull OnBackPressedDispatcher dispatcher) {
if (mLifecycleOwner == null) {
throw new IllegalStateException("You must call setLifecycleOwner() before calling "
+ "setOnBackPressedDispatcher()");
}
// Remove the callback from any previous dispatcher
mOnBackPressedCallback.remove();
// Then add it to the new dispatcher
dispatcher.addCallback(mLifecycleOwner, mOnBackPressedCallback);
}
//NavController#mOnBackPressedCallback
private final OnBackPressedCallback mOnBackPressedCallback =
new OnBackPressedCallback(false) {
@Override
public void handleOnBackPressed() {
popBackStack(); // 最终回调Navigator#popBackStack
}
};
<br/>
8. SwipeBack
Navigation没有提供滑动返回的能力,我们从Fragmentation中找到解决方案:onCreateView的时候,将SwipeLayout作为Container容器
使用方式非常简单:
class SwipeBackFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_swipe_back, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
swipeBackLayout.setEnableGesture(true) //一句话开启SwipeBack
}
}
借助ReportFragment代理,避免了额外基类的引入。swipeBackLayout
是扩展属性,实际获取的是parentFragment(ReportFragment)
的实例
val Fragment.swipeBackLayout
get() = (parentFragment as ReportFragment).swipeBackLayout
ReportFragment中的处理非常简单,将SwipeLayout作为Container即可
internal class ReportFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
swipeBackLayout =
SwipeBackLayout(requireContext()).apply {
attachToFragment(
this@ReportFragment,
inflater.inflate(R.layout.report_layout, container, false)
.apply { appendBackground() } // add a default background color to make it opaque
)
setEnableGesture(false) //default false
}
return swipeBackLayout
}
为了避免滑动过程中的背景穿透,调用applyBackgroud()
为Fragment添加与当前主题相同的默认背景色
private fun View.appendBackground() {
val a: TypedArray =
requireActivity().theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
val background = a.getResourceId(0, 0)
a.recycle()
setBackgroundResource(background)
}
<br/>
9. Show Dialog
Activity通过设置Theme可以以Dialog样式启动,使用DialogFragment
同样可以实现Dialog样式的Fragment。Navigation对DialogFragment已经做了支持,Fragivity只要调用相关方法即可:
9.1 定义DialogFragment
class DialogFragment : DialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_dialog, container, false)
}
}
9.2 显示Dialog
navigator.showDialog(DialogFragment::class)
DialogFramgent也需要在Graph上动态添加Destination,只是与普通的Fragment有所区别,其配套的Navigator类型是DialogFragmentNavigator
:
//创建Destination
val destination = DialogFragmentNavigatorDestinationBuilder(
navigatorProvider[DialogFragmentNavigator::class],
destId, clazz ).apply {
label = clazz.qualifiedName
}.build()
//添加到Graph
graph.plusAssign(destination)
<br/>
最后
Fragivity在核心逻辑上力求最大程度复用Navigation的能力,并保持与最新版本同步,这有利于保证框架的先进性和稳定性。同时Fragivity致力于打造与Activity相近的使用体验,以帮助开发者更低成本地转向单Activity架构。
工程源码中有本文介绍的各种API的demo,欢迎大家下载体验,提issue,觉得好用别忘了start
https://github.com/vitaviva/fragivity