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
为什么我要自己写一个Navigation

这个路由结构现已写好了

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,工作量和危险着实不小。

本文合适的场景有限,所以仅当给我们拓宽个思路。假如有不合理和考虑不周的当地,也期望能够和我们友好讨论。

最后假如本文对你有帮助,就求点赞求评论求保藏,给个一键三连吧~