上一篇文章讲了一些关于 Activity
和 Fragment
的一些零碎的知识点,只有深入的了解了它们,我们才能合理的运用它们。UI相比于数据流,更灵活也更混乱,合理运用不同组件,可以使得条例更清晰,代码量更少。
合理运用Activity
与 Fragment
虽然我们经常在说单 Activity
多 Fragment
的架构,但官方推荐的架构并不是单 Activity
多 Fragment
的架构,如果我们去看他的文档或示例代码,我们可以得到官方一个推荐的职责划分:
Activity
用于模块,而 Fragment
用于流程
例如官方一个用户注册模块,一个 RegisterActivity
表示注册,然后有 RegisterUserNameFragment
、RegisterAvatarFragment
等来表示注册的各个步骤,它们都公用同一个数据对象,那么我们就可以把数据放在 RegisterActivity
的 ViewModel
里。而注册流程结束后,我们释放 RegisterActivity
时,同时也释放了注册相关的数据。这是一个比较优雅的方式:我们即实现了数据的跨页面使用,又在流程结束后将数据及时释放。作为最佳实践,如果我们的多个界面(Fragment
)需要用到同一批数据,那么我们就可以用一个 Activity
来包裹这些 Fragment
。
举一个反面例子,有些同学彻底贯彻单 Activity
多 Fragment
的架构来实现多 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() } }
这样我们就来到了主页了。