1. 首先什么是 Fragment?
Fragment 是 Android 的视图生命周期控制器, 可以把它看做一个轻量级的 Activity, 与传统的 Activity 相比, 它只占用更少的资源, 并且提供更大的编码灵活性, 在超低版本上的兼容性等.
使用 Fragment, 即使是在肥肠差劲的平台 (例如 API 19 以下连 ART 都没有的的老系统) 上也能得到较好的运行效果, 并且能将过渡动画兼容到更低的版本(通过 FragmentTransition 指定).
早期的 Fragment 出现过很多问题, 比如没有 onBackPressed(), 没有启动模式, 重复创建, 辣鸡的回退栈, 迷之生命周期等等, 导致很多开源作者自己独立开发了用于 Fragment 管理的框架, 其中比较出名的有 YoKeyword 大佬的 Fragmentation.
不过事物总是曲折发展的, 经过 Google 多年的调教, 现在的 Fragment 的功能已经很完善了, 在很多场合, 足以在很多场合替代 Activity 的存在, 上面的一些问题也得到了比较妥善的解决, 如果看完这篇文章, 相信你会找到答案.
image.PNG
巨佬 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 进阶实践技术
来源: http://www.jianshu.com/p/610b8baecb06