本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
1.首先什么是Fragment?
Fragment
是Android
的视图生命周期控制器(带生命周期的自定义View
),是Activity
上的View
层级中的一部分,一般可以把它看做一个轻量级的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
的功能太过强大以至于耦合了View
层和Model
层),它的开销是很大的,单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
有两种方式生成,一是硬编码到xml
文件中,二是在Java
代码中new
,然后通过FragmentManager#beginTransaction
开启FragmentTransaction
提交来添加Fragment
(下文会介绍).两种方式存在着一定区别.硬编码到xml
的Fragment
无法被FragmentTransition#remove
移除,与Activity
同生共死,所以你要是这么用了,就不用试了,移除不了的,但是在代码中new
出来的是可以被移除的.
直接硬编码到xml
中:
"@+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
要在什么位置显示.
//在xml中编写放置Fragment位置的容器
"@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
//在java代码中动态添加Fragment
requireFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, fragment)
.runOnCommit(()->{/*TODO*/})
.addToBackStack(null)
.commit();
复制代码
在Fragment
中,我们可以使用getId()
可以返回自身的id
,通常用这个方法返回它所在的容器的id
,供其他Fragment
添加进也添加到当前容器时使用(例如使用Fragment
返回栈的场景)。
/**
* 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;
}
复制代码
需要注意的是FragmentTransaction
并不是立即执行的,而是在当前代码执行完毕后,回到事件循环(也就是你们知道的Looper
)时,才会执行,不过他会保证在下一帧渲染之前得到执行(通过Handler#createAsync
机制),若要在FragmentTransaction
执行时搞事情,你需要使用runOnCommit
,在上面的代码中我使用了Java8
的lambda
表达式简写了Runnable
.
如果你还想使用Fragment
回退栈记得调用addToBackStack
,最后别忘了commit
,这样才会生效,此时commit
函数返回的是BackStackEntry
的id
当然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
把它设计成交给开发者自行管理了.
这个功能是完全基于Google
的appcompat
包实现的,但是若是我们想要使用这个功能,可能需要较高版本的appcompat
包,或者你把项目迁移到AndroidX
(迁移方式下面会介绍).
我们可以使用FragmentActivity
(AppCompatActivity
继承了FragmentActivity
)的addOnBackPressedCallback
方法为你的Fragment
提供拦截OnBackPressed
的功能了.(非AndroidX
的其他版本可能也有实现了这个功能)
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
的最前面的.
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
的管理下,否则就会报错
好了如果你现在使用的appcompat
包没有上面的骚操作.那么下面我将带你迁移到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 Studo3.0
以上中的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=true
复制代码
为了消除这些难看的红线,你可以直接将新的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
)
最后补充一个小Tips
:当我们在使用Fragment#getActivity()
时返回的是一个可空值,如果没有判空检查在Android Studio
中将会出现一个恶心的黄色警告,你可以使用requireActivity()
来代替它,同样的方法还有requireFragmentManager()
等.
3.Fragment生命周期
这可能是最让人懊恼的部分之一了。它彰显了Fragment
中最让人恐惧的一部分,它的生命周期.
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) {
}
复制代码
本来笔者想要用ProcessOn,自己画一张Fragment
生命周期的流程图.,最后......真香,因为这图实在是太复杂了,真要画它时间上有点过不去,所以我只好拿来主义.
下图展示了各回调发生的时间顺序(出处在这):
捋一下,觉得上面有图有点烦的话的话那就看下面总结的文字吧,常用的回调有这些:
-
onInflate(Context,AttributeSet,Bundle)
只有硬编码在xml
中的Fragment
(即使用fragment
标签)才会回调此方法,这与自定义View
十分类似,在实例化xml
布局时该方法会被调用,先于onAttach
. -
onAttach(Context)
执行该方法时,Fragment
与Activity
已经完成绑定,当一个Fragment
被添加到FragmentManager
时,如果不是在xml
中直接定义fragment
标签,那么该方法总是最先被回调.该方法传入一个Context
对象,实际上就是该Fragment
依附的Activity
.重写该方法时记得要调用父类的super.onAttach
,父类的onAttach
调用返回后,此时调用getActivity
将不会返回null
,但是Activity#onCreate
可能还有没有执行完毕(如果是在xml
中定义,这种情况就会发生,因为此时这个回调的这个发生的时间也就是你在Activity#onCreate
里setContentView
的时间,直到Fragment#onViewCreated
返回之后,Activity#onCreate
才会继续执行)。 -
onCreate(Bundle)
用来初始化Fragment
。它总是在onAttach
执行完毕后回调,可通过参数savedInstanceState
获取之前保存的值,记得一定要调用父类的super.onCreate
。 -
onCreateView(LayoutInflater,ViewGroup,Bundle)
需要返回一个View
用来初始化Fragment
的布局,它总是在onCreate
执行完毕后回调。默认返回null
,值得注意的是,若返回null
Fragment#onViewCreated
将会被跳过,且如果是在xml
中定义fragment
标签并用name
指定某个Fragment
,则这个方法不允许返回null
,否则就会报错。当使用ViewPager
+Fragment
时此方法可能会被多次调用(与Fragment#onDestroyView
成对调用)。 -
onActivityCreated(Bundle)
执行该方法时,与Fragment
绑定的Activity
的onCreate
方法已经执行完成并返回,若在此方法之前与Activity
交互交互没有任何保证,引用了未初始化的资源就会应发空指针异常。 -
onStart()
执行该方法时,Fragment
所在的Activity
由不可见变为可见状态 -
onResume()
执行该方法时,Fragment
所在的Activity
处于活动状态,用户可与之交互. -
onPause()
执行该方法时,Fragment
所在的Activity
处于暂停状态,但依然可见,用户不能与之交互,比如Dialog
盖住了Activity
-
onStop()
执行该方法时,Fragment
所在的Activity
完全不可见 -
onSaveInstanceState(Bundle)
保存当前Fragment
的状态。该方法会自动保存Fragment
的状态,比如EditText
键入的文本,即使Fragment
被回收又重新创建,一样能恢复EditText
之前键入的文本,说实话我不太喜欢这个方法,保存到Bundle
里的设计实在是太蠢了,不过好在现在已经有了代替它的方案,Google
的Android Jetpack MVVM
框架,之后我也会专门出一篇文章来介绍。 -
onDestroyView()
销毁与Fragment
有关的视图,但未与Activity
解除绑定,一般在这个回调里解除Fragment
对视图的引用。通常在ViewPager
+Fragment
的方式下会使用并重写此方法,并且与Fragment#onCreateView
一样可能是多次的。 -
onDestroy()
销毁Fragment
。通常按Back
键退出或者Fragment
被移除FragmentManager
时调用此方法,此时应该清理Fragment
中所管理的所有数据,它会在onDetach
之前回调。 -
onDetach()
解除与Activity
的绑定。在onDestroy
方法之后调用。Fragment
生命周期的最末期,若在super.onDetach
返回后getActivity()
,你将会得到一个null
。
4.Fragment的替代方案
看了那么多有关Fragment
的介绍,如果你还对Fragment
嗤之以鼻,又想减小业务的逻辑的粒度,那么我只能给你Fragment
的替代方案了。
一位square
公司(对就是那个诞生了Retrofit
和okhttp
的公司)的工程师开发的Fragment
替代方案《View框架flow》,以及相关博文,国内有优秀的简书作者翻译了这篇文章《(译)我为什么不主张使用Fragment》,原作者在这篇文章中痛斥了Fragment
的各种缺点,我想你可能会喜欢这个.
5.结语
好了关于从Activity
迁移到Fragment
的介绍差不多就到这了,我也是想到什么就写什么,所以文章的结构可能会有些乱(逃......),以后如果还有其他知识点我会慢慢补充上来.
如果你喜欢我的文章记得给我点个赞,拜托了,这对我真的很重要.