這篇文章是這個系列的第四篇文章了,下面是前三篇文章:
按照慣例,放一下 Github 地址和 apk 下載地址吧!
apk 下載地址:www.pgyer.com/llj2
Github地址:github.com/zhujiang521…
為什麼要寫這一篇文章?感覺寫著寫著又回到了原點。
在第一篇文章的評論中,有下面這麼一條:
在第一篇文章中我們搭建了 BaseActivity 和 BaseFragment,不清楚的可以去看下第一篇文章:玩安卓從 0 到 1 之總體概覽。裡面將一些公共用到的方法抽取了出來,還把 LCE 的操作:比如顯示錯誤、載入失敗、載入內容、網路錯誤等等狀態都放在了 BaseActivity 和 BaseFragment 中。
本來以為這樣寫挺方便,在需要不同狀態的頁面直接將 LCE 的頁面 include 進去即可,但是當看見這個叫 alienzh 的哥們評論之後,我也感覺到了自己這樣寫確實不好,因為這個小專案中很多頁面都需要 LCE,每個頁面都需要 include 一遍,在寫這個小專案的時候就覺得不對,每次還需要為了將 LCE 頁面新增進去而新增一個 FrameLayout 將頁面包裹起來,無形中就多巢狀了一層佈局,比如下面這個佈局:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.project.list.ProjectListFragment">
<com.scwang.smartrefresh.layout.SmartRefreshLayout
android:id="@+id/offListSmartRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/offListRecycleView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.scwang.smartrefresh.layout.SmartRefreshLayout>
<include
layout="@layout/layout_lce"/>
</FrameLayout>
本來一層的佈局直接搞成了這樣,看著也不美觀。所以就想著按照這個哥們的思路來搞一波嘗試下!
翻了下官方檔案,發現在 Activity 中有個叫 addContentView 的方法,它不會移除先前新增的UI元件,會將新新增的空間累積上去,這不正好符合需求嘛!說幹就幹:
val view = View.inflate(this, R.layout.layout_lce, null)
val params = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
params.setMargins(0,
ConvertUtils.dp2px(if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) 70f else 55f),0,0
)
addContentView(view, params)
直接通過 View 來把 LCE 的佈局 inflate 進來,然後根據橫豎屏來將 TitleBar 的高度預留出來,不然顯示的時候就沒有頭佈局了。
下面需要做的就很簡單了,和之前一樣就行:
loading = view.findViewById(R.id.loading)
noContentView = view.findViewById(R.id.noContentView)
badNetworkView = view.findViewById(R.id.badNetworkView)
loadErrorView = view.findViewById(R.id.loadErrorView)
loadFinished()
和之前一樣進行 findViewById 即可,只不過需要通過剛剛 inflate 的 View 來 findViewById,最後別忘記加上 loadFinished(),因為預設是要能正常顯示佈局的。
OK了!很簡單,但是省了很大的事,好多地方會用到。
是不是有人納悶我為什麼要分的這麼清楚,Fragment 和 Activity 不是一樣嘛!直接還用 addContentView 方法不得了嘛!我最初也是這樣想的,但是後來發現自己想錯了。。。。。。
為什麼想錯了呢?大家可以去 Fragment 中看看,根本沒有這樣類似的方法啊(也許有,但我沒找見,知道的可以在評論區告訴我,感激不盡)!
這。。。咋辦呢?
先來看下咱們平時寫 Fragment 的時候怎樣載入佈局吧:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(getLayoutId(), container, false)
}
上面的 getLayoutId() 是個抽象方法,用來獲取子類的佈局。
發現了沒?直接 return 了一個 inflate 出來的 View,那麼這就好說了。
再來想一下,咱們的目的是什麼,是要把 LCE 的佈局給新增進去,在上面的佈局檔案中咱們是怎樣操作的?沒錯,用了一個 FrameLayout 包裹了一下,然後裡面放了一個 LCE 的佈局,既然 View 已經知道是什麼了,那咱們自己用程式碼建立一個 FrameLayout 來包裹不就可以了嘛!說幹就幹:
val frameLayout = FrameLayout(context!!)
很簡單,下面直接用 View 來把 LCE 佈局給 inflate 進來:
val lce = View.inflate(context, R.layout.layout_lce, null)
val params = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
val isPort = resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
params.setMargins(0,ConvertUtils.dp2px(if (isPort) 70f else 55f),0,0)
lce.layoutParams = params
現在也拿到 LCE 的 View 了,FrameLayout 咱們也建立出來了,原本的佈局用抽象方法已經拿到了,萬事俱備,只欠把這兩個佈局新增進去了,來看下最後的程式碼:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val frameLayout = FrameLayout(context!!)
val lce = View.inflate(context, R.layout.layout_lce, null)
val params = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
val isPort = resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
params.setMargins(0,ConvertUtils.dp2px(if (isPort) 70f else 55f),0,0)
lce.layoutParams = params
val content = inflater.inflate(getLayoutId(), container, false)
frameLayout.addView(content)
frameLayout.addView(lce)
onCreateView(lce)
return frameLayout
}
這不就可以了嘛!是不是有種恍然大明白的感覺!這裡需要注意一下,frameLayout 在 addView 的時候一定要注意先後順序,我在這裡吃過虧,之前順序搞反了,結果 LCE 佈局的點選時間無法進行使用,後來才發現要把 LCE 放在上面,也就是在後面 addView 就可以了。
上面的 BaseActivity 和 BaseFragment 中將 LCE 佈局提取到了父類別中,雖然減輕了一些子類的負擔,但還是感覺有哪塊不對勁,咱們來看下之前子類中觀察 LiveData 的程式碼:
viewModel.getData().observe(this, Observer {
if (it.isSuccess) {
loadFinished()
val projectTree = it.getOrNull()
if (projectTree != null) {
// 執行操作
} else {
showLoadErrorView()
}
} else {
showBadNetworkView(View.OnClickListener { initData() })
}
})
基本上 ViewModel 中用到 LiveData 的都是相同的流程,那麼也可以抽出來啊,之前一直不知道該怎樣進行抽取,但後來想了下,寫一個方法,將 LiveData 傳入進去,在回撥出來在子類進行對應的操作不得了!
說幹就幹,先來看第一版程式碼:
fun <T> setDataStatus(dataLiveData: LiveData<Result<T>>){
dataLiveData.observe(this){
if (it.isSuccess) {
val articleList = it.getOrNull()
if (articleList != null) {
loadFinished()
setData(articleList)
} else {
showLoadErrorView()
}
} else {
showBadNetworkView { initData() }
}
}
}
protected open fun <T> setData(data: T){
}
來簡單說下上面程式碼的意思吧!引數很簡單,就是將 LiveData 傳進來,然後進行判斷,然後在成功獲取資料的地方對資料進行賦值,讓子類實現 setData 方法進行對應操作,來隨便看一個子類的寫法吧:
setDataStatus(viewModel.projectTreeLiveData)
直接將 LiveData 扔進去,然後接下來重寫 setData 方法:
override fun <T> setData(data: T){
data as List<ProjectClassify>
// 進行對應操作
}
是不是也不難,但是好像感覺哪裡不對,咋還需要強轉一下呢?應該是直接獲取到對應型別才對啊!當時感覺走到了死衚衕,背後好多路等著走偏不回頭,非得死磕,還想到了 Kotlin 的泛型實化、行內函式、crossinline,但後來一想都沒啥關係啊!
有時候寫程式碼就是這樣,思路一下子定住就出不來了!後來一想在方法上再接受一個介面回撥不得了,於是又有了第二版:
fun <T> setDataStatus(dataLiveData: LiveData<Result<T>>, onDataStatus: DataStatusListener<T>) {
dataLiveData.observe(this) {
if (it.isSuccess) {
val dataList = it.getOrNull()
if (dataList != null) {
loadFinished()
onDataStatus.onDataStatus(dataList)
} else {
showLoadErrorView()
}
} else {
showBadNetworkView { initData() }
}
}
}
interface DataStatusListener<T> {
fun onDataStatus(t: T)
}
這樣不就可以了嘛!來看下使用方法有什麼改變:
setDataStatus(dd.getDataLiveData(), collect -> {
// 執行對應操作
});
這樣只是增加了個藉口就完美解決了剛才那樣需要強轉的問題,不對!這是 Kotlin 啊,不需要藉口回撥啊,Kotlin 可以都幹掉啊,高階函數不就是幹這個事的嘛!腦子真的瓦特掉了!
fun <T> setDataStatus(dataLiveData: LiveData<Result<T>>, onDataStatus: (T) -> Unit) {
dataLiveData.observe(this) {
if (it.isSuccess) {
val dataList = it.getOrNull()
if (dataList != null) {
loadFinished()
onDataStatus(dataList)
} else {
showLoadErrorView()
}
} else {
showBadNetworkView { initData() }
}
}
}
這樣寫不香嘛😂!搞那麼多花裡胡哨的!要什麼藉口,不要了!
這個專案我接入了騰訊的 Bugly 來檢視使用中出現的 Crash,發現一直有個問題:
這就給我整懵逼了,知道是哪塊程式碼出了問題,但就是不知道該怎樣改,百度、Google 找了不知道多久都沒有一絲頭緒,先給大家看下出問題的程式碼:
protected open fun fragmentManger(position: Int) {
mViewModel.setPage(position)
val targetFg: Fragment = mFragments!![position]
val transaction = mFragmentManager!!.beginTransaction()
if (currentFragment != null) {
transaction.hide(currentFragment!!)
}
if (!targetFg.isAdded) {
transaction.add(R.id.flHomeFragment, targetFg).commit()
} else {
// 這裡報錯
transaction.show(targetFg).commit()
}
currentFragment = targetFg
}
很簡單的一段程式碼,只是切換了個 Fragment 而已,就一直報上面的錯誤,大家也可以隨便去百度,這個問題當時給我噁心壞了,總感覺應該是一個很小的錯誤導致的,但就是找不到這個錯誤在哪!
這種感覺很噁心,但還是會經常遇到。我也不詳細描述解決的過程吧,挺艱辛的,但解決方法和原因都非常簡單。。。。
來看下問題詳情:
一看問題描述就知道是因為 HomePageFragment 已經 attached 了 FragmentManager 了,就不能再次 attached。問題很簡單,但為啥呢???為啥不行呢,其他地方也沒有錯誤啊!
最後,罪魁禍首竟然是因為我使用了單例。。。。。
object FragmentFactory {
private val mHomeFragment: HomePageFragment by lazy { HomePageFragment.newInstance() }
private val mProjectFragment: ProjectFragment by lazy { ProjectFragment.newInstance() }
private val mObjectListFragment: OfficialAccountsFragment by lazy { OfficialAccountsFragment.newInstance() }
private val mProfileFragment: ProfileFragment by lazy { ProfileFragment.newInstance() }
fun getCurrentFragment(index: Int): Fragment? {
return when (index) {
0 -> mHomeFragment
1 -> mProjectFragment
2 -> mObjectListFragment
3 -> mProfileFragment
else -> null
}
}
}
之前為了 Fragment 能夠重用而不用重新新建而建立的單例,結果一切問題都是因為它!因為單例導致生命週期不一致從而引發的問題!看來以後單例也不敢瞎用了!一定要考慮清楚。
解決方法很簡單,直接將 Fragment 放到空間中,保持生命週期一致即可,這裡就不貼程式碼了,和上面程式碼是一致的。想看的可以去 Github 下載程式碼看:com.zj.play.view.main.BaseHomeBottomTabWidget。
也寫了不少了,亂七八糟說了一大堆,這一篇文章並沒有繼續往前寫這個小專案,而是回頭來看了下是否應該這樣寫,感覺比之前的幾篇文章更有用。
能力一般、水平有限,對大家有幫助的話別忘了三連,有 Github 賬號的幫忙點個 Star ,感激不盡!
就這樣,下回再見!!!