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

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

前言

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

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,发现一直有个问题:

e1a35b993d1f241c640036c9e5af8ecc.png

问题原因

这就给我整懵逼了,知道是哪块代码出了问题,但就是不知道该怎样改,百度、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 而已,就一直报上面的错误,大家也可以随便去百度,这个问题当时给我恶心坏了,总感觉应该是一个很小的错误导致的,但就是找不到这个错误在哪!

这种感觉很恶心,但还是会经常遇到。我也不详细描述解决的过程吧,挺艰辛的,但解决方法和原因都非常简单。。。。

来看下问题详情:

9e0610a7f34c85ef2369cf086afd08e1.png



一看问题描述就知道是因为 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

总结

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




目录
相关文章
|
25天前
|
安全 Android开发 iOS开发
深入探索Android与iOS的差异:从系统架构到用户体验
在当今的智能手机市场中,Android和iOS无疑是最受欢迎的两大操作系统。本文旨在探讨这两个平台之间的主要差异,包括它们的系统架构、开发环境、安全性、以及用户体验等方面。通过对比分析,我们可以更好地理解为何不同的用户群体可能会偏好其中一个平台,以及这些偏好背后的技术原因。
|
1月前
|
Android开发 Swift iOS开发
深入探索iOS与Android操作系统的架构差异及其对应用开发的影响
在当今数字化时代,移动设备已经成为我们日常生活和工作不可或缺的一部分。其中,iOS和Android作为全球最流行的两大移动操作系统,各自拥有独特的系统架构和设计理念。本文将深入探讨iOS与Android的系统架构差异,并分析这些差异如何影响应用开发者的开发策略和用户体验设计。通过对两者的比较,我们可以更好地理解它们各自的优势和局限性,从而为开发者提供有价值的见解,帮助他们在这两个平台上开发出更高效、更符合用户需求的应用。
|
2月前
|
前端开发 JavaScript 测试技术
android做中大型项目完美的架构模式是什么?是MVVM吗?如果不是,是什么?
android做中大型项目完美的架构模式是什么?是MVVM吗?如果不是,是什么?
122 2
|
2月前
|
存储 前端开发 Java
Android MVVM架构模式下如何避免内存泄漏
Android采用MVVM架构开发项目,如何避免内存泄漏风险?怎样避免内存泄漏?
109 1
|
18天前
|
存储 Linux API
深入探索Android系统架构:从内核到应用层的全面解析
本文旨在为读者提供一份详尽的Android系统架构分析,从底层的Linux内核到顶层的应用程序框架。我们将探讨Android系统的模块化设计、各层之间的交互机制以及它们如何共同协作以支持丰富多样的应用生态。通过本篇文章,开发者和爱好者可以更深入理解Android平台的工作原理,从而优化开发流程和提升应用性能。
|
19天前
|
安全 Android开发 iOS开发
深入探索iOS与Android系统架构差异及其对开发者的影响
本文旨在通过对比分析iOS和Android两大移动操作系统的系统架构,探讨它们在设计理念、技术实现及开发者生态方面的差异。不同于常规摘要仅概述内容要点,本摘要将简要触及核心议题,为读者提供对两大平台架构特点的宏观理解,铺垫
|
18天前
|
网络协议 Linux Android开发
深入探索Android系统架构与性能优化
本文旨在为读者提供一个全面的视角,以理解Android系统的架构及其关键组件。我们将探讨Android的发展历程、核心特性以及如何通过有效的策略来提升应用的性能和用户体验。本文不包含常规的技术细节,而是聚焦于系统架构层面的深入分析,以及针对开发者的实际优化建议。
32 1
|
22天前
|
IDE 安全 Android开发
深入探索Android与iOS操作系统的架构差异
本文旨在对比分析Android和iOS两大主流移动操作系统在架构设计上的根本差异。通过详细解读两者的系统架构、开发环境、以及安全性等方面,揭示它们各自的特点及优势,为开发者选择合适的平台提供参考。
|
1月前
|
Java Linux Android开发
深入探索Android系统架构:从Linux内核到应用层
本文将带领读者深入了解Android操作系统的复杂架构,从其基于Linux的内核到丰富多彩的应用层。我们将探讨Android的各个关键组件,包括硬件抽象层(HAL)、运行时环境、以及核心库等,揭示它们如何协同工作以支持广泛的设备和应用。通过本文,您将对Android系统的工作原理有一个全面的认识,理解其如何平衡开放性与安全性,以及如何在多样化的设备上提供一致的用户体验。
|
1月前
|
安全 Android开发 iOS开发
深入探讨Android与iOS的系统架构差异
本文旨在通过对比分析Android和iOS两大移动操作系统的系统架构,揭示它们在设计理念、安全性、应用生态及开发环境等方面的显著差异。我们将从底层架构出发,逐步剖析至用户界面层面,为开发者和科技爱好者提供一份详尽的技术参考。
33 1
下一篇
DataWorks