QMUI實戰(三)——你是如何啟動你的第一個 Fragment 的?

古哥發表於2019-12-22

上一篇文章講了一些關於 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()
    }
}
複製程式碼

這樣我們就來到了主頁了。

下期博文:QMUI實戰(四)—— QMUI 換膚的實現與使用

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

相關文章