美文网首页
《Android编程权威指南》之Fragment Navigat

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

作者: 夜远曦白 | 来源:发表于2021-09-20 21:10 被阅读0次

    《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

    相关文章

      网友评论

          本文标题:《Android编程权威指南》之Fragment Navigat

          本文链接:https://www.haomeiwen.com/subject/lenniltx.html