Android技术栈(一)从Activity迁移到Fragmen
1.首先什么是Fragment?
Fragment
是Android
的视图生命周期控制器,可以把它看做一个轻量级的Activity
,与传统的Activity
相比,它只占用更少的资源,并且提供更大的编码灵活性
、在超低版本上的兼容性
等.
使用Fragment
,即使是在肥肠差劲的平台(例如API 19以下连ART
都没有的的老系统)上也能得到较好的运行效果,并且能将过渡动画兼容到更低的版本(通过FragmentTransition
指定)。
早期的Fragment
出现过很多问题,比如没有onBackPressed()
,没有启动模式,重复创建,辣鸡的回退栈,迷之生命周期等等,导致很多开源作者自己独立开发了用于Fragment
管理的框架,其中比较出名的有YoKeyword大佬的Fragmentation.
不过事物总是曲折发展的,经过Google
多年的调教,现在的Fragment
的功能已经很完善了,在很多场合,足以在很多场合替代Activity
的存在,上面的一些问题也得到了比较妥善的解决,如果看完这篇文章,相信你会找到答案。
巨佬JakeWharton
曾经建议:一个App
只需要一个Activity
.
这说的就是单Activity
多Fragment
模式.使用这种模式有许多好处:
- 首先第一个好处就是流畅,要知道
Activity
属于系统组件,受AMS
管理并且自身是一个God Object
,它的开销是很大的,单Activity
模式可以为我们节省很多资源,还可以避免资源不足时,被前台Activity
覆盖的Activity
被杀掉导致页面数据丢失的情况(因为只有一个Activity
,除非JAVA
堆内存到达系统要杀掉一个程序的临界点,否则系统最不倾向于杀死前台正在运行的Activity
); - 其次就是可以将业务逻辑拆分成更小的模块,并将其组合复用,这在这在大型软件系统中尤为重要(新版
知乎
就使用了单Activity
多Fragment
这种模式),因为我们都知道Activity
的是无法在多个页面中复用的,而此时Fragment
就有了它的勇武之地,它作为轻量级的Activity
,基本可以代理Activity
的工作,并且他是可复用 - 再者,使用
Fragment
可以为程序带来更大的灵活性,我们都知道在Activity
之间传递对象,对象需要序列化,这是因为Activity
作为系统组件,是受AMS
管理的,而AMS
属于系统进程,不在当前程序运行的进程中,启动Activity
时需要暂时离开当前进程去到AMS
的进程中,而AMS
则会将你准备好的数据(也就是Intent
之类的)用来启动Activity
,这也是Fragment
和Activity
之间的区别之一,Activity
属于系统组件,可以在别的进程运行(组件化/多进程方案),而Fragment
只是框架提供给我们的的一个组件,它必须依附于Activity
生存,并且只能在当前进程使用,但这同时也意味这它可以获得更大的灵活性,我们可以给Fragment
传递对象而无需序列化,甚至可以给Fragment
传递View
之类的对象,这都是Activity
不容易做到的.
2.要使用Fragment你必须知道的一些事情
首先要提一点,如果你要学习Fragment
那么你至少得是掌握了Activity
的,如果你还不了解Activity
,笔者建议你先去看一些Activity
相关的文章,再来进阶Fragment
.从下面的文章开始,默认读者已经了解了Activity
的生命周期等相关知识。
Fragment
拥有Activity
所有的生命周期回调函数并且由于自身特点还扩展了一些回调函数,但是这些与Activity
相关的回调函数几乎只与Fragment
依附的Activity
有关,如果不熟悉Fragment
,很容易凭直觉造成误会.例如,一个Fragment
并不会因为在Fragment
回退栈上有其他Fragment
把它盖住,又或者是你使用FragmentTransition
将它hide
而导致他onPause
,onPause
只跟此Fragment
依附的Activity
有关,这在Fragment
的源码中写得清清楚楚.
/**
* Called when the Fragment is no longer resumed. This is generally
* tied to {@link Activity#onPause() Activity.onPause} of the containing
* Activity's lifecycle.
*/
@CallSuper
public void onPause() {
mCalled = true;
}
那当我们想在Fragment
不显示时做一些事情要怎么办呢?我们有onHiddenChanged
回调,当Fragment
的显示状态通过FragmentTransition
改变时(hide
和show
),就会回调这个函数,参数hidden
将告诉你这个Fragment
现在是被隐藏还是显示着.
/**
* Called when the hidden state (as returned by {@link #isHidden()} of
* the fragment has changed. Fragments start out not hidden; this will
* be called whenever the fragment changes state from that.
* @param hidden True if the fragment is now hidden, false otherwise.
*/
public void onHiddenChanged(boolean hidden) {
}
Fragment
有两种方式生成,一是硬编码到xml
文件中,二是在Java
代码中new
,然后通过FragmentManager#beginTransaction
开启FragmentTransaction
提交来添加Fragment
(下文会介绍).两种方式存在着一定区别.硬编码到xml
的Fragment
无法被FragmentTransition#remove
移除,与Activity
同生共死,所以你要是这么用了,就不用试了,移除不了的,但是在代码中new
出来的是可以被移除的.
硬编码到xml
中:
<fragment
android:id="@+id/map_view"
android:name="org.kexie.android.dng.navi.widget.AMapCompatFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
添加Fragment
的第二种方式就是使用FragmentManager#beginTransaction
(代码如下)动态添加,你需要先new
一个Fragment
,然后通过下面Fragment#requireFragmentManager
获取FragmentManager
来使用beginTransaction
添加Fragment
,注意add
方法的第一个参数,你需要给它指定一个id
,也就是Fragment
容器的id
,通常容器是一个没有子View
的FrameLayout
,它决定了Fragment
要在什么位置显示。
需要注意的是FragmentTransaction
并不是立即执行的,而是在当前代码执行完毕后,回到事件循环(也就是你们知道的Looper
)时,才会执行,不过他会保证在下一帧渲染之前得到执行(通过Handler#createAsync
机制),若要在FragmentTransaction
执行时搞事情,你需要使用runOnCommit
,下面的代码中我使用了Java8
的lambda
表达式简写了Runnable
.
若要使用Fragment
回退栈记得addToBackStack
,最后别忘了commit
,这样才会生效,此时commit
函数返回的是BackStackEntry
的id
requireFragmentManager()
.beginTransaction()
.add(id, fragment)
.runOnCommit(()->{/*TODO*/})
.addToBackStack(null)
.commit();
当然FragmentTransaction
不止可以执行add
操作,同样也可以执行remove
,show
,hide
等操作.
这里插入一个简短的题外话作为上面知识的补充。如何在Android Studio
中启用Java8
?在你模块的build.gradle
中
android{
//省略.....
//加上下面的脚本代码,然后sync你的项目
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
onBackPressed
在哪?我知道第一次使用Fragment
的人肯定都超想问这个问题.众所周知Fragment
本身是没有onBackPressed
的.不是Google
不设计,而是真的没法管理啊!!!,如果一个界面上有三四个地方都有Fragment
存在,一按回退键,谁知道要交给哪个Fragment
处理呢?所以Fragment
是"没有"onBackPressed
的.
在这里我的“没有”打了引号,因为实际上给Fragment
添加类似onBackPressed
的功能的办法是存在的,只是Google
把它设计成交给开发者自行管理了.
要想使用Fragment
的onBackPressed
,你可能需要先升级到AndroidX
.
这里可能有人会问AndroidX
是什么?
简单来讲AndroidX
就是一个与平台解绑的appcompat
(低版本兼容高版本功能)库,也就是说在build.gradle
中不需要再与compileSdkVersion
写成一样,例如之前这样的写法:
compile 'com.android.support:appcompat-v7:24.+'
(注:使用24.+则表明使用 24. 开头的版本的最新版本,若直接使用+号则表明直接使用该库的最新版本。
现在可以写成:
implementation 'androidx.appcompat:appcompat:1.1.0-alpha02'
(注:新的依赖方式implementation
与compile
功能相同,但是implementation
无法在该模块内引用依赖的依赖,但compile
可以,这么做的好处是可以加快编译速度。新的依赖方式api
与compile
完全相同,只是换了名字而已)
在Android Studo
中的Refactor->Migrate to AndroidX
的选点击之后即可将项目迁移到AndroidX
,在确认的时会提示你将项目备份以免迁移失败时丢失原有项目,通常情况下不会迁移失败,只是迁移的过程会花费很多的时间,如果项目很大,迁移时间会很长,这时即使Android Studio
的CPU
利用率为0
也不要关闭, 但是如果发生迁移失败,这时候就需要手动迁移了。
一些使用gradle
依赖的一些第三方库中的某些类可能继承了android.support.v4
包下的Fragment
,但迁移到AndroidX
后appcompat
的Fragment
变成了androidx.fragment.app
包下,原有的代码下会画红线,Android Studio
也会警告你出现错误,但是不用担心,依然可以正常编译,Android Studio
在编译的时候会自动完成基类的替换,但前提是你要确保你项目里的gradle.properties
进行了如下设置。
android.useAndroidX=true
android.enableJetifier=tru
为了消除这些难看的红线,你可以直接将新的Fragment
使用这种方式强制转换成原有的Fragment
。
TextureSupportMapFragment mapFragment = TextureSupportMapFragment
.class.cast(getChildFragmentManager()
.findFragmentById(R.id.map_view));
同理,也可以将旧的Fragment
强制类型转换成新的Fragment
.
Fragment f = Fragment.class.cast(mapFragment);
(注:上面的TextureSupportMapFragment
是一个典型案例,他是高德地图SDK
中的Fragment
,本身已经继承了v4包下的Fragment
,可以用过上面的转换来使他兼容AndroidX
)
差点扯远了,搞定AndroidX
后,我们就可以使用FragmentActivity
的addOnBackPressedCallback
方法为你的Fragment
提供拦截OnBackPressed
的功能了.
public void addOnBackPressedCallback(@NonNull LifecycleOwner owner,
@NonNull OnBackPressedCallback onBackPressedCallback)
OnBackPressedCallback#handleOnBackPressed
需要返回一个boolean
值。如果你在这个回调里拦截了onBackPressed
应该返回true
,说明你自己已经处理了本次返回键按下的操作,这样你的Fragment
就不会被弹出返回栈了。
值得注意的是,这个函数的第一个参数,一个LifecycleOwner
,Activity
和Fragment
都是LifecycleOwner
,用于提供组件的生命周期,这个参数可以帮我们自动管理OnBackPressedCallback
回调,你无需手动将他从Activity
中移除,在LifecycleOwner
的ON_DESTROY
事件来到的时候,他会被自动移除列表,你无需担心内存泄漏,框架会帮你完成这些事情。
/**
* Interface for handling {@link ComponentActivity#onBackPressed()} callbacks without
* strongly coupling that implementation to a subclass of {@link ComponentActivity}.
*
* @see ComponentActivity#addOnBackPressedCallback(LifecycleOwner, OnBackPressedCallback)
* @see ComponentActivity#removeOnBackPressedCallback(OnBackPressedCallback)
*/
public interface OnBackPressedCallback {
/**
* Callback for handling the {@link ComponentActivity#onBackPressed()} event.
*
* @return True if you handled the {@link ComponentActivity#onBackPressed()} event. No
* further {@link OnBackPressedCallback} instances will be called if you return true.
*/
boolean handleOnBackPressed();
}
我们可以看到Activity
内管理的OnBackPressedCallback
的执行循序与添加时间有关.最后被添加进去的能最先得到执行.
public void addOnBackPressedCallback(@NonNull LifecycleOwner owner,
@NonNull OnBackPressedCallback onBackPressedCallback) {
Lifecycle lifecycle = owner.getLifecycle();
if (lifecycle.getCurrentState() == Lifecycle.State.DESTROYED) {
// Already destroyed, nothing to do
return;
}
// Add new callbacks to the front of the list so that
// the most recently added callbacks get priority
mOnBackPressedCallbacks.add(0, new LifecycleAwareOnBackPressedCallback(
lifecycle, onBackPressedCallback));
}
可以看到它是添加到mOnBackPressedCallbacks
这个List
的最前面的.
Fragment
是必须要有id
的,使用getId
可以返回自身的id
,通常用这个方法返回它所在的容器的id
,供其他Fragment
添加进FragmentManager
时使用。(比如说你使用了一个FrameLayout
作为Fragment
的容器,那么它就会返回那个FrameLayout
的id
)
/**
* Return the identifier this fragment is known by. This is either
* the android:id value supplied in a layout or the container view ID
* supplied when adding the fragment.
*/
final public int getId() {
return mFragmentId;
}
startFragmentForResult
方法在哪?对不起和OnBackPressed
一样,Google
没有直接为我们实现这个方法,但这并不代表Fragment
没有这个功能,你当然可以直接用定义getter
的方式来获取Fragment
上内容,但这并不是最佳实践,为了规范编码我们最好还是使用公共的API
Fragment#setTargetFragment
可以给当前Fragment
设置一个目标Fragment
和一个请求码
public void setTargetFragment(@Nullable Fragment fragment, int requestCode)
当当前Fragment
完成相应的任务后,我们可以这样将返回值送回给我们的目标Fragment
通过Intent
getTargetFragment().onActivityResult(getTargetRequestCode(),
Activity.RESULT_OK,new Intent());
不过要注意,目标Fragment
和被请求的Fragment
必须在同一个FragmentManager
的管理下,否则就会报错
最后,当我们在使用Fragment#getActivity()
时返回的是一个可空值,如果没有判空检查在Android Studio
中将会出现一个恶心的黄色警告,你可以使用requireActivity()
来代替它,同样的方法还有requireFragmentManager()
等.
3.Fragment生命周期
这可能是最让人懊恼的部分之一了。下面是绝对的高能区,因为接下来的那一张图,彰显了Fragment
中最让人恐惧的一部分,它的生命周期.
本来笔者想要用ProcessOn,自己画一张Fragment
生命周期的流程图.怎么说我都是软件工程
专业的啊,最后......真香,因为这图实在是太复杂了,真要画它时间上有点过不去,所以我只好拿来主义.
下图展示了各回调发生的时间顺序:
image.png捋一下,常用的回调有这些,觉得上面有图有点烦的话的话那就看下面总结的文字吧:
-
onInflate(Context,AttributeSet,Bundle)
只有硬编码在xml
中的Fragment
(即使用fragment
标签)才会调用该方法,与自定义View
十分类似,在实例化xml
布局时该方法会被调用 -
onAttach(Context)
执行该方法时,Fragment
与Activity
已经完成绑定,该方法传入一个Context
对象,实际上就是该Fragment
依附的Activity
,此时调用getActivity
将不会返回null
,但是Activity#onCreate
可能还有没有执行。 -
onCreate(Bundle)
用来初始化Fragment
。可通过参数savedInstanceState
获取之前保存的值。 -
onCreateView(LayoutInflater,ViewGroup,Bundle)
需要返回一个View
用来初始化Fragment
的布局。默认返回null
,值得注意的是,若返回null
Fragment#onViewCreated
将不会执行。使用ViewPager
+Fragment
时此方法可能会被多次调用。 -
onActivityCreated(Bundle)
执行该方法时,与Fragment
绑定的Activity
的onCreate
方法已经执行完成并返回,若在此之前与Activity
交互,若引用了未初始化的资源会应发空指针异常。 -
onStart()
执行该方法时,Fragment
所在的Activity
由不可见变为可见状态 -
onResume()
执行该方法时,Fragment
所在的Activity
处于活动状态,用户可与之交互 -
onPause()
执行该方法时,Fragment
所在的Activity
处于暂停状态,但依然可见,用户不能与之交互 -
onStop()
执行该方法时,Fragment
所在的Activity
完全不可见 -
onSaveInstanceState(Bundle)
保存当前Fragment
的状态。该方法会自动保存Fragment
的状态,比如EditText
键入的文本,即使Fragment
被回收又重新创建,一样能恢复EditText
之前键入的文本。 -
onDestroyView()
销毁与Fragment
有关的视图,但未与Activity
解除绑定,一般在这个回调里解除Fragment
对视图的引用。通常在ViewPager
+Fragment
的方式下会使用并重写此方法,并且与Fragment#onCreateView
一样可能是多次的。 -
onDestroy()
销毁Fragment
。通常按Back
键退出或者Fragment
被移除FragmentManager
时调用此方法,此时应该清理Fragment
中所管理的所有数据。 -
onDetach()
解除与Activity
的绑定。在onDestroy
方法之后调用。若在此时getActivity()
,你将会得到一个null
。
4.Fragment的替代方案
看了那么多有关Fragment
的介绍,如果你还对Fragment
嗤之以鼻,又想减小业务的逻辑的粒度,那么我只能给你Fragment
的替代方案了。
一位square
公司(对就是那个诞生了Retrofit
和okhttp
的公司)的工程师开发的Fragment
替代方案《View框架flow》,以及相关博文,国内有优秀的简书作者翻译了这篇文章《(译)我为什么不主张使用Fragment》,原作者在这篇文章中痛斥了Fragment
的各种缺点,我想你可能会喜欢这个.
5.结语
好了关于从Activity
迁移到Fragment
的介绍差不多就到这了,我也是想到什么就写什么,所以文章的结构可能会有些乱,以后如果还有其他知识点我会慢慢补充上来.
【附】相关架构及资料
Android高级技术大纲资料及源码领取
点赞+加群免费获取 Android IOC架构设计
领取获取往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术