前言
这篇文章是这个系列的第四篇文章了,下面是前三篇文章:
按照惯例,放一下 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>
本来一层的布局直接搞成了这样,看着也不美观。所以就想着按照这个哥们的思路来搞一波尝试下!
解决
BaseActivity增加LCE
翻了下官方文档,发现在 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了!很简单,但是省了很大的事,好多地方会用到。
BaseFragment增加LCE
是不是有人纳闷我为什么要分的这么清楚,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
总结
也写了不少了,乱七八糟说了一大堆,这一篇文章并没有继续往前写这个小项目,而是回头来看了下是否应该这样写,感觉比之前的几篇文章更有用。