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

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

上一篇文章讲了一些关于 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<? 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()
    }
}

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

目录
相关文章
|
SQL 缓存 安全
GreenDao查询,Querying
GreenDao查询,Querying
|
8月前
|
安全 数据挖掘 BI
|
人工智能 架构师 测试技术
【云故事探索】NO.7:「越用越上瘾」,中华财险60%研发人员用通义灵码提效
保险业被视为社会“稳定器”与经济“助推器”,正快速数字化转型。中华联合财产保险股份有限公司运用“云+大模型”技术革新业务,通过阿里云的通义灵码大幅提升编码效率,近60%的研发人员采用,采纳的生成代码占比约20%,显著提升了团队创新能力与代码质量,并积极探索大模型在更多垂直业务场景的应用。
【云故事探索】NO.7:「越用越上瘾」,中华财险60%研发人员用通义灵码提效
|
前端开发 JavaScript 开发者
console.log()
【8月更文挑战第29天】
450 5
|
XML 缓存 Android开发
QMUI实战(二)—Activity 和 Fragment,我们该选择谁?
在一开始,官方只提供了 Activity 来作为 UI 界面的载体,因此我们也别无选择,只能用它。而在 Android 3.0 后,Fragment 也面世了,它一开始是用于适配平板的,以邮件列表与详情的适配为例,手机端够小,因此开始展示列表,点击进入详情,而平板够大,则可以列表显示在左侧,详情显示在右侧,点击列表只是切换详情。对于这种适配场景,列表页和详情页必须在同一个 Activity 里了,而这便是我所知道的 Fragment 诞生的场景了。
359 0
|
JavaScript 前端开发 API
深入浅出:使用Node.js打造简易Web API
【8月更文挑战第31天】本文旨在通过一个简单实例,引导读者快速入门Node.js并创建自己的Web API。我们将从零开始,一步步搭建起服务端应用,涉及环境搭建、基本语法、路由处理等关键知识点,最后以代码实例加深理解。无论你是前端开发者还是后端新手,这篇文章都能让你轻松上手,体验后端开发的乐趣。
|
人工智能 C# 云计算
C#编程的未来发展趋向
【4月更文挑战第21天】C#编程未来将深化跨平台支持,强化云计算与容器技术集成,如.NET Core、Docker。在AI和ML领域,C#将提供更丰富框架,与AI芯片集成。语言和工具将持续创新,优化异步编程,如Task、async和await,提升多核性能。开源生态的壮大将吸引更多开发者,共创更多机遇。
406 4
|
Android开发
android Jetpack Navigation组件——堆栈操作和动画效果
android Jetpack Navigation组件——堆栈操作和动画效果
922 0
android Jetpack Navigation组件——堆栈操作和动画效果
|
Java Maven
maven配置阿里云镜像源
maven配置阿里云镜像源
40215 1
|
设计模式 测试技术 数据库
基于Android的食堂点餐APP的设计与实现(论文+源码)_kaic
基于Android的食堂点餐APP的设计与实现(论文+源码)_kaic