《Android编程权威指南》之Fragment Navigation

《Android编程权威指南》第12章了,本章要学习 Fragment 的替换的哟~ 还有它们之间传递数据,以及学习使用 LiveData transformation 响应 UI 状态变化加载不可变数据。

一、单Activity多Fragment

这是一种 只含一个 activity + 多个fragment 的架构。其中 activity 责任重大,负责响应用户事件,交替使用各个fragment。

理论上说,Fragment 是一种可组装的独立部件,为维护 fragment 的独立性,应在 fragment 里面定义回调接口,把不该它做的事都交给它的托管 activity 来做。像管理调度 fragment 以及决定布局依赖关系这样的任务,就让托管 activity 通过实现回调接口去完成。而不应该直接从 fragment 里面拿到 actvity 去完成。

  • Fragment 回调接口
class CrimeListFragment : Fragment() {
...
private var callBacks: CallBacks? = null
...
override fun onAttach(context: Context) {
        super.onAttach(context)
        callBacks = context as CallBacks?
    }

 override fun onDetach() {
        super.onDetach()
        callBacks = null
    }
...

interface CallBacks {
        fun onCrimeSelected(crimeId: UUID)
    }
}

现在,CrimeListFragment 可调用其托管 activity 的函数了。至于托管 activity 是谁并不重要,只要它实现 CrimeListFragment.callbacks 接口,CrimeListFragment 都一样工作。

private inner class CrimeHolder(val itemBinding: ItemCrimeBinding) :
        RecyclerView.ViewHolder(itemBinding.root) {
...
  init {
            itemBinding.root.setOnClickListener {
                /*Toast.makeText(
                    context,
                    "${mCrime.title} is pressed",
                    Toast.LENGTH_SHORT
                ).show()*/
                callBacks?.onCrimeSelected(mCrime.id)
            }
        }
...
}
  • 替换 fragment
class MainActivity : AppCompatActivity(), CrimeListFragment.CallBacks {
...
override fun onCrimeSelected(crimeId: UUID) {
        Log.d(TAG, "MainActivity.onCrimeSelected : $crimeId")
        val fragment = CrimeFragment.newInstance()
        supportFragmentManager.beginTransaction()
            .replace(R.id.flayout_fragment_container, fragment)
            .commit()
    }
}

此时还没加回退栈呢,那么按了返回键就直接退出了应用了。再加个回退栈,可以取个名字,它是可选的。

supportFragmentManager.beginTransaction()
            .replace(R.id.flayout_fragment_container, fragment)
            .addToBackStack(null)
            .commit()

二、Fragment argument

每个 fragment 实例都可以附带一个 fragment argument Bundle 对象。主要用于 Fragment 数据传递的。

  • 将 argument 附加到 fragment
private const val ARG_CRIME_ID = "crime_id"
class CrimeFragment : Fragment() {
...
  companion object {
        fun newInstance(crimeId:UUID) : CrimeFragment{
            val args = Bundle().apply {
                putSerializable(ARG_CRIME_ID, crimeId)
            }
            return CrimeFragment().apply { arguments = args }
        }
    }
...
}

注意,activity 和 fragment 不需要也无法同时相互保持独立。MainActivity 必须了解 CrimeFragment 的内部细节,以便托管 fragment,但 fragment 不需要知道其托管 activity 的细节问题,这样就保证了 fragment 独立。

  • 获取 argument
class CrimeFragment : Fragment() {
...
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        crime = Crime()
        val crimeId: UUID = arguments?.getSerializable(ARG_CRIME_ID) as UUID
        Log.d(TAG, "args bundle crime ID:$crimeId")
    }
...
}

三、使用LiveData数据转换

需添加 CrimeDetailViewModel 来管理数据库查询,并管理不同查询后的数据更新。

class CrimeDetailViewModel : ViewModel() {

    private val crimeRepository = CrimeRepository.get()
    private val crimeIdLiveData = MutableLiveData<UUID>()

    var crimeLiveData: LiveData<Crime?> = Transformations.switchMap(crimeIdLiveData) {
        crimeRepository.getCrime(it)
    }

    fun loadCrime(crimeId: UUID) {
        crimeIdLiveData.value = crimeId
    }
}

crimeIdLiveData 保存着 CrimeFragment 当前显示(或将要显示)的 crime 对象的 ID。CrimeDetailViewModel 刚创建时,这个crime ID还没有设置。但最终,CrimeFragment 会调用 CrimeDetailViewModel.loadCrime(UUID) 以让ViewModel 知道该加载哪个 crime 对象。

一般来讲,ViewModel 从不应该对外暴露 MutableLiveData。

LiveData 数据转换(live data transformation)是设置两个 LiveData 对象之间触发和反馈关系的一个解决办法。一个数据转换函数需要两个参数:一个用作触发器(trigger)的 LiveData 对象,一个返回 LiveData 对象的映射函数(mappingfunction)。数据转换函数会返回一个数据转换结果(transformation result)——其实就是一个新 LiveData 对象。每次只要触发器 LiveData 有新值设置,数据转换函数返回的新 LiveData 对象的值就会得到更新。

class CrimeFragment : Fragment() {
...
private val crimeDetailViewModel: CrimeDetailViewModel by lazy {
        ViewModelProvider(this).get(CrimeDetailViewModel::class.java)
    }
 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mCrime = Crime()
        val crimeId: UUID = arguments?.getSerializable(ARG_CRIME_ID) as UUID
        Log.d(TAG, "args bundle crime ID:$crimeId")
        crimeDetailViewModel.loadCrime(crimeId)
    }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        crimeDetailViewModel.crimeLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer {
            it?.let {
                this.mCrime = it
                updateUI(it)
            }
        })
    }

    private fun updateUI(crime:Crime) {
        mBinding.edtCrimeTitle.setText(crime.title)
        mBinding.btnCrimeDate.apply {
            text = crime.date.toString()
            isEnabled = false
        }
        mBinding.cboxCrimeSolved.apply {
            isChecked = crime.isSolved
            jumpDrawablesToCurrentState()
        }
    }
...
}

上面 jumpDrawablesToCurrentState() 函数是为了跳过 checkbox 的勾选动画。

四、更新数据库

crime 数据只能保存在数据库里,而 crime 明细的页面是可以对 crime 的数据状态进行修改的,那么修改了之后。

先在 CrimeDao.kt 中添加插入数据和更新数据的函数,默认情况下,所有查询都必须在单独的线程上执行,而 Room 支持 Kotlin 协程,所以这里相对书上做点小修改,用 suspend 修饰符对查询进行注解,然后从协程或其他挂起函数对其进行调用。

@Dao
interface CrimeDao {
...
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertCrime(crime: Crime)

    @Update
    fun updateCrime(crime: Crime)
}
  • 使用 executor

书中为了在创建新线程调用数据库的更新插入,使用到的是 Executor 技术,有关 Executor 介绍可参考:https://developer.android.com/reference/kotlin/java/util/concurrent/Executor

还有有关Android线程池的详解:https://blog.csdn.net/wangbaochu/article/details/53941424

当然也可以直接使用协程,目的就是为了不在 UI 线程直接操作数据库,会造成 ANR。

优先照着书中代码敲一遍熟悉熟悉喽。再可以自己进行一些练习修改。由于我原先为了预先插入数据,就用过协程把自创了一些 crime 数据插入到数据库,所以,这里练习就仅仅更新数据库数据库操作跟着书中示例代码敲。

...
class CrimeRepository private constructor(context: Context) {
...
private val executor = Executors.newSingleThreadExecutor()
...
    fun insertCrimes(crime: Crime) {
        GlobalScope.launch {
            crimeDao.insertCrime(crime)
        }
    }

    fun updateCrime(crime: Crime) {
        executor.execute { crimeDao.updateCrime(crime) }
    }

newSingleThreadExecutor() 函数会返回一个指向新线程的 executor 实例。使用这个 executor 实例执行的工作都会发生在它指向的后台进程上。

  • 数据库写入与 fragment 生命周期

在 CrimeDetailViewModel.kt 文件增加函数,将数据写入数据库。

class CrimeDetailViewModel : ViewModel() {
...
    fun saveCrime(crime: Crime) {
        crimeRepository.updateCrime(crime)
    }
}

然后重写 CrimeFragment 的 onStop() 方法,只要页面不可见了就保存数据。

class CrimeFragment : Fragment() {
...
    override fun onStop() {
        super.onStop()
        crimeDetailViewModel.saveCrime(mCrime)
    }
...
}

五、深入学习:为何要用 Fragment Argument

防止设备配置改变时,fragment 重建数据丢失。

因为 activity 会重建,fragment 也会重建,新 fragment 依然会被添加给新 activity,可是 fragment 重建的时候默认调用 fragment 的无参构造函数。那么,新 fragment 就把传参数据丢失了。可是 Fragment Argument 可以在 fragment 被销毁的情况依然保存,在 fragment 重建的时候把保存的 argument 重新赋给新 fragment。即使用 onSaveInstanceState(Bundle) 来防止数据丢失,维护成本也很高。

六、深入学习:Navigation架构组件库

https://developer.android.google.cn/guide/navigation/navigation-getting-started?hl=zh-cn

有关 Navigation 使用介绍如上地址,可以尝试将代码进行修改。完成了整本书再来实践,嘿嘿,代码将即使更新到 Github。

七、挑战练习:实现高效的RecyclerView刷新

由于修改 crime 详情都只是修改了一条 item,而回到列表页面则在刷新整个列表,效率实在太低,现在呢,要求提高 RecyclerView 的刷新效率,每次回到列表页面只更新当前修改过的那条记录。

此时呢,需要修改 Adapter ,从原先继承 RecyclerView.Adapter<CrimeHolder> 改为继承androidx.recyclerview.widget.ListAdapter<Crime, CrimeHolder>。

private inner class CrimeAdapter(var crimes: List<Crime>) :
        ListAdapter<Crime, RecyclerView.ViewHolder>(CrimeDiffCallback()) {
...
}

ListAdapter 是一个RecyclerView.Adapter,它能找出支持RecyclerView的新旧数据之间的差异,然后告诉它只重绘有变化的数据。新旧数据的比较在后台线程上完成,所以不会拖慢UI反应。

ListAdapter使用androidx.recyclerview.widget.DiffUtil 来决定哪一部分的数据发生了变化。再实现DiffUtil.ItemCallback<Crime>回调函数。

class CrimeDiffCallback : DiffUtil.ItemCallback<Crime>() {

        override fun areItemsTheSame(oldItem: Crime, newItem: Crime): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: Crime, newItem: Crime): Boolean {
            return oldItem.toString() == newItem.toString()
        }
    }

另外,你还需要更新CrimeListFragment,提交更新后的crime列表给RecyclerView的adapter。调用ListAdapter.submitList(MutableList<T>?)函数提交一个新列表,或者配置LiveData,观察数据变化。

有关 ListAdapter 官方介绍:https://developer.android.com/reference/androidx/recyclerview/widget/ListAdapter

八、其他

CriminalIntent 项目 Demo 地址:
https://github.com/visiongem/AndroidGuideApp/tree/master/CriminalIntent

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容