Fragment的地位在提升
传统来说,Android APP中的页面应该是以多个Activity去组织的,Fragment往往只合适在Activity中挖出一块,用于展现便于切换的碎片页面。
随着Jetpack Navigation(此处首要指的是navigation-fragment
)的推出,Fragment的地位开始有所提高。Navigation引荐咱们用多个Fragment去展现单个事务下的多个页面,似乎渐渐取代了传统的Activity,当上了主角。
甚至,咱们能够考虑一整个app都只在一个Activity容器上承载,所有页面都经过Fragment去完结,这便是Single activity application。早在多年前,Android官方推出Navigation时就提出了这种想象(Single activity: Why, when, and how (Android Dev Summit ’18))。这样做有哪些激动人心的改变呢?
- 更轻量的完结:Fragment比Activity更轻量,也不需求在AndroidManifest中界说
- 更好的性能:发动Activity触及与体系服务的跨进程通讯,而发动Fragment则简略得多
- 信息传递:经过同享ViewModel去传递参数,比经过Intent去给另一个Activity传参更简略和灵活
- 大局弹窗:是否遇到过弹窗还没处理的时分发生了Activity跳转,此刻弹窗就被挤掉了。只有一个Activity时,弹窗便是大局的,取得和iOS相同的大局弹窗体会。
- 无需申请权限的使用内浮窗:咱们知道使用起浮窗口是需求向体系申请相关权限的,假如咱们只需求一个使用内的“浮窗”,那只需往Activity的布局上添加这个“浮窗”,它就能够“起浮”在所有页面的顶上,得到一个使用内浮窗的效果。
⚖ 那价值呢
凡事总有利害,Single Activity Application带来优点的同时也引入了一些危险:
- Fragment的生命周期比Activity更杂乱
- Fragment的回退栈欠好管理,且调试时无法用adb指令dump出来
- 屏幕方向等Activity装备难以管理
为了便利开发者完结多Fragment的路由,Jetpack推出了Navigation这个最早是用于操控Fragment路由导航的结构。
Navigation好用吗
作为官方推出的结构,介绍它的文章自有不少,这儿就不翻开。
我也亲自使用过一段时间,确实能处理一些问题,但也同时有很多痛点
- 用xml界说路由表,与代码界说的Fragment有点分裂,且写法杂乱
- 无法坚持之前的Fragment状况
- 除了自行操控,在进入或回来时,不确定能否坚持Fragment的屏幕方向,是否全屏等特点
- 用id资源来做路由地址,除非用DeepLink
- 短少路由阻拦器机制
写完Fragment后还要去navGraph的xml去界说一下,实在是麻烦,我甚至连layout的xml都不想写
和layout xml说拜拜 BrickUI,根据Android View体系撸一个声明式UI结构
假如让我来写一个Fragment路由结构
我开始考虑,假如我要去做一个Single activity application,我需求一个怎样的路由结构?
- 直接在Fragment上界说路由信息
- 能够挑选是否坚持历史Fragment的状况
- 能够在去到或回到Fragment时,就像Activity相同,恢复其横竖屏、全屏等窗口特点,而不需求额外操控
- 能够经过uri来装备路由地址和传参,一个页面支撑装备多个路由地址
- 具有路由阻拦器机制,阻拦器能够动态装载和卸载,阻拦器有优先级区别
- 已然能传参,那还应该能够回来成果给上一个页面,相似onActivityResult
- 支撑相似Activity的LaunchMode
这个路由结构现已写好了
github.com/robin8yeung…
Blink,名字取自dota游戏中的闪烁技术。欢迎我们来star一下⭐️⭐️⭐️
来看看blink-fragment怎么用
界说Activity容器
容器Activity用于承载Fragment,为了使blink-fragment
结构正常运转,有以下要求:
- 需求承继抽象类
BlinkContainerActivity
- 制止体系设置改变导致Activity重建
class FragmentContainerActivity: BlinkContainerActivity() {
// 首个展现的Fragment,不期望写死也能够回来null,后续经过blink()方法来跳转
override fun startFragment() = HomeFragment()
// 其他事务代码
}
因为Activity重建会导致一系列问题,不太好处理,如成果回来,状况保护等,所以现阶段制止Activity重建,请在AndroidManifest.xml
中对容器Activity的android:configChanges
进行以下装备:
<activity android:name="com.seewo.blink.example.fragment.FragmentContainerActivity"
android:configChanges="mcc|mnc|navigation|orientation|touchscreen|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"/>
界说一个Fragment
经过注解即可在界说Fragment的当地界说好它的路由地址,以及它的一系列页面特点。当然,这些页面特点的界说不是有必要的。
object Uris {
const val fragment = "blink://my.app/fragment"
const val HOME = "blink://my.app/home"
}
// 为MyFragment界说一个或多个路由uri
@BlinkUri(value = [Uris.fragment, Uris.HOME])
// 界说页面方向为竖屏,当来到或回到这个页面时,屏幕方向都将切换为竖屏
@Orientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
// 自界说转场动画
@CustomAnimations(
enter = R.anim.enter_from_bottom, exit = R.anim.fade_out,
popEnter = R.anim.fade_in, popExit = R.anim.exit_to_bottom)
// 界说页面进入回退栈后不再坚持状况(即经过replace切换到新的页面)
@KeepAlive(false)
// 设置SystemUI样式,当来到或回到此页面时,SystemUI样式更新为以下装备
@SystemUI(
hideStatusBar = true,
hideNavigationBar = true,
brightnessLight = false,
)
// 设置页面的布景色彩,相似于Activity设置window的布景色彩
@Background(Color.TRANSPARENT)
class MyFragment : Fragment() {
// ....
}
LaunchMode
前面说了,咱们还能够界说LaunchMode,但不是经过注解来界说。不是不可,而是相似Activity需求在onNewIntent中去接纳二次翻开时的新Intent。而blink-fragment界说了相关抽象类来供给相应功用。
// 为MyFragment界说LaunchMode为singleTop,承继SingleTopFragment即可
@BlinkUri(Uris.fragment)
class MyFragment : SingleTopFragment() {
override fun onNewArguments(arguments: Bundle?) {
// 重复翻开时,会回调此方法
}
}
// 为MyFragment界说LaunchMode为singleTask,承继SingleTaskFragment即可
@BlinkUri(Uris.fragment)
class MyFragment : SingleTaskFragment() {
override fun onNewArguments(arguments: Bundle?) {
// 重复翻开时,会回调此方法
}
}
这儿没有SingleInstance形式,需求的话能够自行开一个新的Activity。
路由表初始化
只用@BlinkUri
界说了路由地址实践上还无法生效,它只是便于初始化路由表在履行KSP时搜集信息。
关于多module的项目,每个界说过@BlinkUri
的module中,都需求完结一个RouteMetadata,在初始化的时分,如Application的onCreate,调用每个RouteMetadata的inject()来把module的路由表注入到大局路由表之中。
假如不期望module的逻辑侵入app module,也能够凭借Jetpack startup结构了来履行module内部的初始化
为什么不必ASM的方法来简化这个过程呢?我的考虑是编译时插桩简略在编译侧形成开销,而这个初始化关于每个module,只需求写一点代码就能够一了百了,终究仍是决议稍微难为一下开发者。
// 用@BlinkMetadata注解界说一个路由表的初始化入口,为了简化完结,请承继BaseMetadata
@BlinkMetadata
class RouteMetadata : BaseMetadata()
// lib module主张用startup结构来完结初始化,也能够在Application的onCreate中对所有模块的BaseMetadata子类进行初始化调用
class AvatarInitializer : Initializer<Unit>{
override fun create(context: Context) {
// 初始化,注入module的路由表到大局路由表,建立uri与页面的映射关系,否则无法完结路由跳转
RouteMetadata().inject()
}
override fun dependencies(): MutableList<Class<out Initializer<*>>> =
mutableListOf()
}
传参加回来
传参加回来回调
blink-fragment根据uri参数来传参,也供给了简练的方法来创立uri。
此外经过blink()
函数的回调参数
即可接纳下个页面回来的数据,是不是比传统Activity的onActivityResult
便利多了?
object Uris {
const val HOME = "blink://my.app/home"
}
// 以下两种Uri的结构方法是等效的,都能够路由到@BlinkUri界说为Uris.HOME的页面并传参
fragment.blink("${Uris.HOME}?name=Peter&age=8") {
// 此处承受回来回调,回来的成果是个Bundle?类型
}
fragment.blink(Uris.HOME.buildUri {
append("name", "Peter")
append("age", "8")
}) {
// 此处承受回来回调,回来的成果是个Bundle?类型
}
接纳参数与回来成果
blink-fragment供给了一系列简略的接纳参数的操作符,也能够经过by lazy的方法来自行处理杂乱的承受参数的操作
@BlinkUri(Uris.HOME)
class HomeFragment : Fragment() {
// 开发者经过by lazy自行处理Name参数传入
private val name: String? by lazy { arguments?.uriOrNull?.getQueryParameter("name") }
// 由Blink供给懒加载函数进行参数注入,默认值可选。
private val age: Int by intParams("age", 18)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
findViewById<View>(R.id.button).setOnClickListener {
// 点击按钮,回来Bundle成果
pop(Bundle().apply {
putInt("result", 1)
})
}
findViewById<View>(R.id.cancel).setOnClickListener {
// 点击取消,直接回来,此刻路由主张方的回调则会接纳到一个null数据
pop()
}
}
}
阻拦器
经过阻拦器能够便利的阻拦某些路由或对路由进行重定向,修改参数等。blink-fragment的阻拦器支撑动态的添加和移除,也支撑优先级的界说
// 这儿仅用于举例,实在使用时,主张阻拦器职责单一
class ExampleInterceptor : Interceptor {
override fun process(from: Fragment?, target: Bundle) {
val uri = target.uriOrNull
// 打印路由信息
Log.i("blink", "[from] $from [to] $uri")
// 获取路由恳求的参数,修改path并增加参数
target.setUri(uri?.build {
path("/another")
append("new", true)
})
// 关于短少权限的状况,阻拦跳转
if (!Permission.hasCameraPermission) {
interrupt("短少必要权限")
}
// 假如权限具有,则继续跑到下一个阻拦器或许跑完了所有阻拦器则履行路由
}
}
val exampleInterceptor = ExampleInterceptor()
// 添加阻拦器
exampleInterceptor.attach()
// 移除阻拦器
exampleInterceptor.detach()
反常处理
已然路由或许被阻拦,就要考虑做反常处理。blink()
函数回来的是一个Result<Unit>
,能够对Result处理反常。
路由失败的原因首要有:
- FragmentNotFoundException 无法找到uri对应的Fragment
-
自界说反常 被路由阻拦,阻拦器调用
interrupt()
时,默认抛InterruptedException
来阻拦阻拦,也支撑自界说阻拦反常
blink("blink://navigator/example?name=Blink").onFailure {
// 处理反常
}.onSuccess {
// 路由成功
}
完结原理
blink-fragment的原理并不杂乱,首要做了几件事:
为每个Fragment分配容器
经过blink-fragment界说的Fragment实践上并不是直接插入BlinkContainerActivity
中的,而是在其外层还包了一层BlinkContainerFragment
,BlinkContainerFragment作为容器,为实践的Fragment供给了布景色彩,特点管理等的相关支撑,也便是实践Fragment经过注解界说的除了BlinkUri以外的特点,都记录在了这个容器中,当来到或回到这个页面时,它就会让这些特点生效,免去Fragment对这些逻辑的关心。
生成路由表
凭借ksp结构,在编译时扫描开发者界说在module内界说的BlinkUri,并为该module生成路由表。再把路由表信息写入到被@BlinkMetadata
注解的类中,为其创立一个_inject()
函数,用于注入大局路由表。终究调用到了这个_inject()函数即可完结路由表的初始化。而_inject()函数的功用,便是往大局路由表单例RouteMap
中注册该module的路由表信息。
履行路由
经过调用blink(uri)
来履行路由导航时,uri会经过每一个阻拦器处理,假如未被阻拦,则终究输出一个终究uri,此刻即可到大局路由表RouteMap
中去查找uri所对应的Fragment。假如无法查找到Fragment,则抛出FragmentNotFoundException
;假如能查找到对应的Fragment,则创立一个BlinkContainerFragment
容器去装载这个Fragment,并且获取其注解的相关参数,并生成一个仅有标识符,终究把这个BlinkContainerFragment根据所注解的参数,装载到BlinkContainerActivity
中
✉️ 成果回来
blink-fragment的路由功用自身根据一个Blink
单例来完结,其也管理了一个搜集回调的映射表,映射表的key为方针Fragment的仅有标识符。当调用pop(bundle)
回来时,经过这个标识符即可查找到对应的成果回调,回调给路由来历
总结
现在blink-fragment
现已接入到一些实践的项目中,也有着不错的开发功率收益。不过假如要做到Single Activity Application,或许关于新项目会更合适,究竟关于成熟项目,把一个个Activity改成Fragment,工作量和危险着实不小。
本文合适的场景有限,所以仅当给我们拓宽个思路。假如有不合理和考虑不周的当地,也期望能够和我们友好讨论。
最后假如本文对你有帮助,就求点赞求评论求保藏,给个一键三连吧~