小松的技术博客

六和敬

若今生迷局深陷,射影含沙。便许你来世袖手天下,一幕繁华。 你可愿转身落座,掌间朱砂,共我温酒煮茶。

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

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

合理运用ActivityFragment

虽然我们经常在说单 ActivityFragment 的架构,但官方推荐的架构并不是单 ActivityFragment 的架构,如果我们去看他的文档或示例代码,我们可以得到官方一个推荐的职责划分:

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

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

举一个反面例子,有些同学彻底贯彻单 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 的产生。

马上都 2020 年了,ViewModel 也应该走进各个 App 了,因此 Activity 一般不需要持有数据了,所以有时候我们并不需要根据模块来新建 Activity 了,我们可以用一个空壳的 Activity,不同的业务模块都用实例化这个空壳 Activity, 然后用 Fragment 来区分和开始处理不同的业务类型。

假设我们使用一个 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

而使用时,只需要在 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 换肤的实现与使用

←微信← →支付宝 →