QMUI实战(三)——你是如何启动你的第一个 Fragment 的?

上一篇文章讲了一些关于 ActivityFragment 的一些零碎的知识点,只有深刻的了解了它们,咱们才能合理的运用它们。UI相比于数据流,更灵活也更混乱,合理运用不一样组件,可使得条例更清晰,代码量更少。java

合理运用ActivityFragment

虽然咱们常常在说单 ActivityFragment 的架构,但官方推荐的架构并非单 ActivityFragment 的架构,若是咱们去看他的文档或示例代码,咱们能够获得官方一个推荐的职责划分:android

Activity 用于模块,而 Fragment 用于流程数组

例如官方一个用户注册模块,一个 RegisterActivity 表示注册,而后有 RegisterUserNameFragmentRegisterAvatarFragment 等来表示注册的各个步骤,它们都公用同一个数据对象,那么咱们就能够把数据放在 RegisterActivityViewModel 里。而注册流程结束后,咱们释放 RegisterActivity 时,同时也释放了注册相关的数据。这是一个比较优雅的方式:咱们即实现了数据的跨页面使用,又在流程结束后将数据及时释放。做为最佳实践,若是咱们的多个界面(Fragment)须要用到同一批数据,那么咱们就能够用一个 Activity 来包裹这些 Fragmentbash

举一个反面例子,有些同窗完全贯彻单 ActivityFragment 的架构来实现多 Fragment 的登陆,当登录完成进入主页后,那就须要销毁登陆的各个 Fragment,其作法就是递归的销毁已经存在的各个 Fragment, 耗时又耗力,并且销毁 Fragment 还可能出翔(上文有提)。可是若是咱们采用一个 LoginActivity 来包裹这些 Fragment, 那就在进入主界面后,直接 finish 掉 LoginActivity,这样不是更简单吗?微信

再以微信读书讲书来举个例子,微信读书讲书点击进去是一个讲书界面,而后讲书界面有一个目录,能够拿到讲书人全部的讲书,当点击目录的 item 时,刷新当前讲书界面。 这是一个比较常见的类型,获得、微课等都有这种界面,那么这个界面你会如何设计呢?我来给下两种实现:数据结构

  1. 用一个 Fragment 承载全部的东西,当切换目录 item 时, 拉取新的讲书详细信息,而后刷新各个 View。
  2. 用一个 Activity,目录数据放在 Activity 的 ViewModel 里,目录 UI 直接挂载在 Activity 上,而后用 Fragment 来承载当前讲书,切换目录 item 时销毁当前讲书 Fragment, 而后建一个新的 Fragment

我想不少人可能会直接选择方案一吧,看上去简单,可是随着业务的增加,显示的逻辑就愈来愈复杂了,例如正常讲书、TTS、公众号讲书,切换目录或推荐时均可能会切换到任意的一种类型,这个时候刷新就是要各类判断,各类差别化处理,痛苦死了,对,这就是微信读书的现状,痛苦得不要不要的。架构

而另一种,列表数据放在了 Activity 层级,从而达到公用,Fragment 只负责特定的讲书,那么这个时候根据不一样的讲书类型实例化出不一样的 Fragment, 数据结构不一致、各类差别化处理都不是问题了。每次切换销毁毁旧而且建立新的 Fragment,仅仅用微乎其微的性能消耗(除非你的 View 巨复杂)就能够换来灵活性、可扩展性、可维护性,从一开始就杜绝了各类 if else 的判断和一些 bug 的产生。app

立刻都 2020 年了,ViewModel 也应该走进各个 App 了,所以 Activity 通常不须要持有数据了,因此有时候咱们并不须要根据模块来新建 Activity 了,咱们能够用一个空壳的 Activity,不一样的业务模块都用实例化这个空壳 Activity, 而后用 Fragment 来区分和开始处理不一样的业务类型。ide

假设咱们使用一个 CommonHolderActivity, QMUI 提供了以下的使用方式,让你能够快速的启动不一样的业务:性能

// 模块 A,以 ModuleAFirstFragment 做为第一个 Fragment
QMUIFragmentActivity.intentOf(context, CommonHolderActivity::class.java, ModuleAFirstFragment::class.java)

// 模块 B,以 ModuleBFirstFragment 做为第一个 Fragment
QMUIFragmentActivity.intentOf(context, CommonHolderActivity::class.java, ModuleBFirstFragment::class.java)

复制代码

接下来我来说讲 QMUIFragmentActivity.intentOf 是如何工做的,以及 @FirstFragments 的用处

First Fragment

First Fragment,是 Activity 里的第一个 Fragment,也是流程的起始。 当咱们已近有了第一个 Fragment 后,接下来的流程主要是经过 QMUIFragment.startFragment() 来启动一个又一个新的 Fragment, 若是流程走完了, 那咱们就是 经过 Activity.finish() 结束整个 Activity。 那么问题来了。 咱们如何为 Activity 添加 First Fragment 呢?

添加 First Fragment 的主体代码以下:

val firstFragment = ...
supportFragmentManager
    .beginTransaction()
    .add(contextViewId, firstFragment, firstFragment.javaClass.getSimpleName())
    .addToBackStack(firstFragment.javaClass.getSimpleName())
    .commit()
复制代码

那么 firstFragment 如何获得呢?在 QMUIDemo 最初的版本是用 if else 去判断的:

// 一些变量来记录启动 First Fragment 是谁?
val DST_FRAGMENT = "dst_fragment"
val DST_HOME = 1
var DST_ARCH = 2
val intent = Intent(context, QDMainActivity::class.java)
intent.put(DST_FRAGMENT, DST_HOME)
startActivity(intent)

// QDMainActivity.java
var firstFragment: QMUIFragment? = null
var dst = intent.getIntExtra(DST_FRAGMENT, DST_HOME)
if(dst == DST_HOME){
   fragment = QDHomeFragment()
}else if(dst == DST_ARCH){
   fragment = QDArchFragment()
}else{
   //.....
}

// supportFragmentManager 添加 firstFragment
复制代码

目前看来,代码量也不是不少,只是简单的几个 if else,而且实现了不一样业务公用同一个 Activity。 但问题是每多一个业务,我就须要加一个变量,而且加一个 else 分支, 短时间没什么,时间久了,就是满屏的 if else 了,至关的不优雅。

有的同窗会采用子类提供 First Fragment 的实现,而放弃公用同一个 Activity

class ParentActivity: QMUIFragmentActivity(){
  override fun onCreate(savedInstanceState: Bundle?) {
     if(savedInstanceState == null){
         val firstFragment = getFirstFragment()
         // supportFragmentManager 添加 firstFragment
     }
  }

  abstract fun getFirstFragment(): QMUIFragemnt
}

class ModuleAActivity: ParentActivity(){
   override fun getFirstFragment() = ModuleAFirstFragment()
}

class ModuleBActivity: ParentActivity(){
   override fun getFirstFragment() = ModuleBFirstFragment()
}
复制代码

但这种实现要写不少 Activity, 而且要在 AndroidManifest 上注册无数次。

为了减小让使用看上去简单一些,我开发了 @FirstFragments 注解来解决这个问题。

其根本思路仍是最开始的 if else 判断,某个变量对应某个 Fragment,但我用代码生成来帮你生成那些变量和 if else 的判断逻辑。 这也是 Android 开发的一个思路,若是是模板式的代码,咱们就能够用代码生成来解决,使得咱们用起来足够舒服就好。其代码生成逻辑也不是很复杂,无非就是一个 Map,Key 为 int, Value 为 Class<? extend QMUIFragment。

而使用时,只须要在 Activity 上声明就行:

@FirstFragments(
    value = [
        HomeFragment::class
    ]
)
class CommonHolderActivity : QMUIFragmentActivity() {}
复制代码

这样咱们就可使用 QMUIFragmentActivity.intentOf

QMUIFragmentActivity.intentOf(context, CommonHolderActivity::class.java, HomeFragment::class.java)
复制代码

若是咱们须要像 First Fragment 传参, 咱们能够启用第四个参数, 固然,这个传参是采用 Fragment.setArguments() 实现的, Fragment 自己要求为无参构造器,这和官方的推荐是一致的。

若是咱们没在 Activity@FirstFragments 数组里加上 Fragment, 那么 QMUIFragmentActivity.intentOf 会抛错的。咱们也可使用 @DefaultFirstFragment 来指定默认的 First Fragment,这时 new Intent(context, CommonHolderActivity::class.java) 就会启用默认的 First Fragment。

实战

好了,理论部分若是搞明白了,代码写起来就简单了。

首先咱们新建 CommonHolderActivity

class CommonHolderActivity : QMUIFragmentActivity() {

    override fun getContextViewId(): Int {
        return R.id.app_common_holder_fragment_container
    }
}
复制代码

这里咱们只须要重写 getContextViewId*( 提供 FragmentContainer 的 id, 那这里可不能够返回 View.generateViewId() 呢? 为何? 若是你读懂了上一篇文章,那么你应该能知道答案。

同时别忘了在 AndroidManifest 里注册:

<activity android:name=".CommonHolderActivity"
    android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout"/>
复制代码

新建 HomeFragment, 并为 CommonHolderActivity 加上注解:

class HomeFragment: QMUIFragment(){
    override fun onCreateView(): View {
        return FrameLayout(context!!).apply {
            val textView = TextView(context).apply {
                text = "第一个 Fragment"
            }
            addView(textView, FrameLayout.LayoutParams(wrapContent, wrapContent).apply {
                gravity = Gravity.CENTER
            })
        }
    }
}

@FirstFragments(
    value = [
        HomeFragment::class
    ]
)
@DefaultFirstFragment(HomeFragment::class)
class CommonHolderActivity : QMUIFragmentActivity() {
    //...
}
复制代码

而后在 LauncherActivity 里补上跳转逻辑:

class LauncherActivity: QMUIActivity(){

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val intent = QMUIFragmentActivity.intentOf(this,
            CommonHolderActivity::class.java,
            HomeFragment::class.java)
        startActivity(intent)
        finish()
    }
}
复制代码

这样咱们就来到了主页了。

下期博文:QMUI实战(四)—— QMUI 换肤的实现与使用

博文地址:blog.cgsdream.org/2019/12/21/…