玩安卓从 0 到 1 之架构思考

   日期:2020-10-29     浏览:89    评论:0    
核心提示:前言这篇文章是这个系列的第四篇文章了,下面是前三篇文章:1、玩安卓从 0 到 1 之总体概览2、玩安卓从 0 到 1 之项目首页3、玩安卓从 0 到 1 之首页框架搭建。按照惯例,放一下 Github 地址和 apk 下载地址吧!apk 下载地址:www.pgyer.com/llj2Github地址:github.com/zhujiang521…起因为什么要写这一篇文章?感觉写着写着又回到了原点。在第一篇文章的评论中,有下面这么一条:在第一篇文章中我们搭建了 BaseActivity

前言

这篇文章是这个系列的第四篇文章了,下面是前三篇文章:

1、玩安卓从 0 到 1 之总体概览

2、玩安卓从 0 到 1 之项目首页

3、玩安卓从 0 到 1 之首页框架搭建。

按照惯例,放一下 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。

总结

也写了不少了,乱七八糟说了一大堆,这一篇文章并没有继续往前写这个小项目,而是回头来看了下是否应该这样写,感觉比之前的几篇文章更有用。

能力一般、水平有限,对大家有帮助的话别忘了三连,有 Github 账号的帮忙点个 Star ,感激不尽!

就这样,下回再见!!!

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服