上一篇文章講了一些關於 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 時,重新整理當前講書介面。 這是一個比較常見的型別,得到、微課等都有這種介面,那麼這個介面你會如何設計呢?我來給下兩種實現:
- 用一個 Fragment 承載所有的東西,當切換目錄 item 時, 拉取新的講書詳細資訊,然後重新整理各個 View。
- 用一個 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()
}
}
複製程式碼
這樣我們就來到了主頁了。
下期博文:QMUI實戰(四)—— QMUI 換膚的實現與使用