首页 > 技术文章 > 《Android 编程权威指南》学习笔记 : 第12章 Fragment Navigation

easy5weikai 2022-05-29 09:00 原文

第12章 Fragment Navigation

本节核心

activity 如何管理 fragment

包括:
- 多个fragment如何交替显示:supportFragmentManager.replace(...)
- 数据在activity与fragment间,fragment内部如何传递:Bundle对象

CrimeFragment 如何更新界面

思路:

  • 点击CrimeListFragment中列表中的某个Item,触发Item的CrimeHolder的点击事件函数,函数内部通过CrimeListFragment的回调接口传递给MainActivity,
  • MainActivity在创建CrimeFragment对象 var fragment = CrimeFragment.newInstance(crimeId) 时传递给CrimeFragment,
  • CrimeFragment将crimeId封装在Bundle对象,赋值给成员变量arguments,这样crimeId可以在CrimeFragment内任何地方获取到,
  • CrimeFragment被附加到MainActivity时, CrimeFragment的生命周期函数onCreate()被调用,在该函数内从arguments中获取crimeId,并传递给其ViewModel,
  • ViewModel把crimeId由封装MutableLiveData,它一变化就会触发数据转函数的执行,从而仓储中获取新的Crime数据从并封装成 crimeLiveData: LiveData<Crime?> ,
  • 在CrimeFragment的生命周期函数onViewCreated()中监听ViewModel的 crimeLiveData: LiveData<Crime?>的数据,并在有变化时更新UI。

单activity多fragment

单activity多fragment如何交替使用多个fragment。
如果fragment获取 activity的FragmentManager来管理fragment的交替,但是这种做法:

  • frament本是个可封装的独立组件,如果这样就得知道它的托管activity 如何工作,它就不再是独立组件了
  • frament得知道activity 的布局中有个为:fragment_container,单这是activity的工作

frament 回调接口

CrimeListFragment 中定义回调接口

好的做法是:frament定义回调接口, 托管的activity必须实现接口,才能托管该fragment。
代码清单:app/src/main/java/com.example.criminalintent/CrimeListFragment.kt

class CrimeListFragment : Fragment() {

    interface Callbacks {
        fun onCrimeSelected(crimeId: UUID)
    }

    private var callbacks: Callbacks? = null
}
  • interface Callbacks { ... } :类内部接口
  • private var callbacks: Callbacks? :保存接口实现类的实例

fragment如何获取接口实现类的实例

当 fragment附加到 activity时,会调用 Fragment.onAttach(Context)生命周期函数,这个Context对象就是托管它的activity实例,
把它保存到 callbacks 变量中

class CrimeListFragment : Fragment() {

    interface Callbacks {
        fun onCrimeSelected(crimeId: UUID)
    }
  
    private var callbacks: Callbacks? = null

    ...

    override fun onAttach(context: Context) {
        super.onAttach(context)
        callbacks = context as Callbacks? // 获取托管fragment的activity实例对象
    }

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

CrimeListFragment 中调用回调接口

怎么调用呢?
最终是在CrimeHolder监听的点击事件中调用 activity实例的接口回调函数callbacks?.onCrimeSelected(crime.id)

代码清单:app/src/main/java/com.example.criminalintent/CrimeListFragment.kt

class CrimeListFragment : Fragment() {

    interface Callbacks {
        fun onCrimeSelected(crimeId: UUID)
    }
  
    private var callbacks: Callbacks? = null

    ...

    override fun onAttach(context: Context) {
        super.onAttach(context)
        callbacks = context as Callbacks? // 获取托管fragment的activity实例对象
    }

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

   ...
    private inner class CrimeHolder(view: View)
        : RecyclerView.ViewHolder(view), View.OnClickListener {

        ...

        init {
            itemView.setOnClickListener(this)
        }

        override fun onClick(v: View?) {
            callbacks?.onCrimeSelected(crime.id) //调用activity实例的接口回调函数
        }
    }
}

MainActivty 实现 fragment的回调接口

代码清单:app/src/main/java/com.example.criminalintent/MainActivity.kt

class MainActivity : AppCompatActivity()
    , CrimeListFragment.Callbacks { // CrimeListFragment 回调接口
   
     ...

    // CrimeListFragment 回调接口
    override fun onCrimeSelected(crimeId: UUID) {
        Log.d(TAG, "MainActivity.onCrimeSelected:$crimeId")
        var fragment = CrimeFragment.newInstance(crimeId)
        supportFragmentManager.beginTransaction()
            .replace(R.id.fragment_container, fragment)
            .addToBackStack(null) // 把事务添加到回退栈,按回退键,能回滚事务,即回退到添加当前fragment之前的状态
            .commit()
    }
}

  • 回调函数onCrimeSelected(crimeId: UUID),crimeId由fragment方传参,这里是 CrimeHolder 调用并传入
  • activity 使用 supportFragmentManager 添加fragment
  • addToBackStack(null),把事务添加到回退栈,按回退键,能回滚事务,即回退到添加当前fragment之前的状态(即:回退到显示CrimeListFragment)
    不加此方法,按回退栈则退出了程序。
  • fragment 由 CrimeFragment 提供方法 newInstance(crimeId) 来创建实例,见下一节。

CrimeFragment

提供创建实例方法,供外部调用

代码清单:app/src/main/java/com.example.criminalintent/CrimeFragment.kt


private const val ARG_CRIME_ID = "crime_id"

class CrimeFragment : Fragment() {
    companion object {
        fun  newInstance(crimeId: UUID): CrimeFragment {
            var args = Bundle().apply {
                putSerializable(ARG_CRIME_ID, crimeId)
            }

            return CrimeFragment().apply {
                arguments = args
            }
        }
    }
}

外部给 Fragment 传递数据

  • Fragment 内置有一个成员 arguments:Bundle,外部的传参可以封装在一个 Bundle 对象中,
    通过 Fragment对象的setArguments()方法给这个内置的成员 arguments设置值,即:
              CrimeFragment().apply {
                  arguments = args
              }
    

Fragment获取外部传递的数据

代码清单:app/src/main/java/com.example.criminalintent/CrimeFragment.kt

private const val ARG_CRIME_ID = "crime_id"

class CrimeFragment : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        crime = Crime()
        val crimeId: UUID = arguments?.getSerializable(ARG_CRIME_ID) as UUID //#3.从 arguments中提取crimeId
        crimeDetailViewModel.loadCrime(crimeId)    //4.ViewModel根据 crimeId 获取信息
    }
    ...
    companion object {
        fun  newInstance(crimeId: UUID): CrimeFragment {  // #1.外部调用 newInstance 传入数据 crimeId
            var args = Bundle().apply {
                putSerializable(ARG_CRIME_ID, crimeId)
            }
         
            return CrimeFragment().apply {
                arguments = args     //#2.crimeId封装在Bundle对象内,并赋值给 arguments:Bundle变量中
            }
        }
    }
}

为什么使用 Fragment Argument 保存 crimeId?

为什么使用 Fragment Argument 保存 crimeId?
因为:
当设备配置改变(如:屏幕旋转)后,当前 activity的Fragment管理器会重建之前托管的fragment,然后把新建的fragment添加给新的activity
Fragment管理器重建fragment时,调用的是fragment的无参构造函数,新的实例无法获取到 crimeId,故把 crimeId 通过构造函数传入的方案行不通!
加之:fragment被销毁,其实例及其内部变量(包括:crimeId)也被销毁.
但是 :
Fragment Argument 会被保存下来,Fragment管理器因设备旋转重建fragment时,会把原来保存的 argument重新附加给新的fragment,
这样,新的fragment就能用它重建自己的状态

总结:数据 crimeId 从 CrimeListFragment > MainActivity > CrimeFragment的流程

参数 crimeId 从 CrimeListFragment > MainActivity > CrimeFragment的整体流程:

  • CrimeListFragment 中的 CrimeHolder的View被点击,CrimeHolder的点击事件函数中调用CrimeListFragment 的接口回调函数传参
class CrimeListFragment : Fragment() {
   ...
    private lateinit var crime: Crime
   ...
    private inner class CrimeHolder(view: View)
        override fun onClick(v: View?) {
            callbacks?.onCrimeSelected(crime.id)  // 调用activity实例的接口回调函数
        }
  • MainActity调用CrimeFragment.newInstance(crimeId),传参crimeId,
class MainActivity : AppCompatActivity()
    , CrimeListFragment.Callbacks { // CrimeListFragment 回调接口

   ...

    // CrimeListFragment 回调接口
    override fun onCrimeSelected(crimeId: UUID) {
     
        var fragment = CrimeFragment.newInstance(crimeId)  
        supportFragmentManager.beginTransaction()
            .replace(R.id.fragment_container, fragment)
            .addToBackStack(null) // 把事务添加到回退栈,按回退键,能回滚事务,即回退到添加当前fragment之前的状态
            .commit()
    }
  • CrimeFragment中把crimeId封装在Bundle对象内,并赋值给自己的成员变量 arguments:Bundle 中
class CrimeFragment : Fragment() {
    ...
    companion object {
        fun  newInstance(crimeId: UUID): CrimeFragment {
            var args = Bundle().apply {
                putSerializable(ARG_CRIME_ID, crimeId)
            }

            return CrimeFragment().apply {
                arguments = args
            }
        }
    }
  • CrimeFragment内部从自己的成员变量 arguments中提取crimeId
class CrimeFragment : Fragment() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        crime = Crime()
        val crimeId: UUID = arguments?.getSerializable(ARG_CRIME_ID) as UUID
        crimeDetailViewModel.loadCrime(crimeId)
    }
  • ViewModel根据 crimeId (从数据库)获取信息
class CrimeFragment : Fragment() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        crime = Crime()
        val crimeId: UUID = arguments?.getSerializable(ARG_CRIME_ID) as UUID
        crimeDetailViewModel.loadCrime(crimeId)
    }

CrimeFragment的UI更新

定义 CrimeFragment的ViewModel

代码清单:app/src/main/java/com.example.criminalintent/CrimeDetailViewModel.kt

class CrimeDetailViewModel : ViewModel() {
    private val crimeRepository: CrimeRepository = CrimeRepository.get()

    /**
    * LiveData与MutableLiveData区别
    * LiveData与MutableLiveData的其实在概念上是一模一样的.唯一几个的区别如下:
    *  1.MutableLiveData的父类是LiveData
    *  2.LiveData在实体类里可以通知指定某个字段的数据更新.
    *  3.MutableLiveData则是完全是整个实体类或者数据类型变化后才通知.不会细节到某个字段
    ** */
    private val crimeIdLiveData = MutableLiveData<UUID>()

    fun loadCrime(crimeId: UUID) {
        crimeIdLiveData.value = crimeId //赋新值,触发LiveData 的数据转换 crimeLiveData的值会得到更新
    }

    /**
     * LiveData 数据转换,第一参数是用做触发器的LiveData对象,每次触发器LiveData对象赋新值,
     * 第二个参数:数据转换函数被执行,返回的新LiveData 对象的值就会得到更新
     * */
    var crimeLiveData: LiveData<Crime?> =
        Transformations.switchMap(crimeIdLiveData) { crimeId ->
            crimeRepository.getCrime(crimeId)
        }
}

CrimeFragment被附加到 MainActivity中,CrimeFragment的生命周期函数 CrimeFragment.onCreate()会被调用

class CrimeFragment : Fragment() {
...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        crime = Crime()
        val crimeId: UUID = arguments?.getSerializable(ARG_CRIME_ID) as UUID
        crimeDetailViewModel.loadCrime(crimeId)
    }
}

crimeDetailViewModel.loadCrime(crimeId)会调用 ViewModel 的 loadCrime(crimeId)方法

    private val crimeIdLiveData = MutableLiveData<UUID>()
    fun loadCrime(crimeId: UUID) {
        crimeIdLiveData.value = crimeId //赋新值,触发LiveData 的数据转换 crimeLiveData的值会得到更新
    }

-LiveData只是抽象类, MutableLiveData才是其实现类。

crimeIdLiveData.value = crimeId, 赋新值,crimeIdLiveData的变化会触发下面代码中的函数Transformations.switchMap的第二个参数:数据转换函数被执行,
返回的新LiveData 对象。

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

,注意:Transformations.switchMap 和 Transformations.Map 的不同,
Transformations.switchMap 会将 crimeRepository.getCrime(crimeId)返回的 LiveData 再转换一次,而不是替换原来的var crimeLiveData: LiveData<Crime?>
因为,必须保证在 CrimeFragment 监听 ViewModel的 crimeLiveData: LiveData<Crime?> 是同一个,因为监听代码只执行一次,要保证监听对象一致。

CrimeFragment 监听 ViewModel的LiveData

代码清单:app/src/main/java/com.example.criminalintent/CrimeDetailViewModel.kt

class CrimeFragment : Fragment() {
    private  val crimeDetailViewModel: CrimeDetailViewModel by lazy {
        ViewModelProvider(this).get(CrimeDetailViewModel::class.java)
    }
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        crime = Crime()
        val crimeId: UUID = arguments?.getSerializable(ARG_CRIME_ID) as UUID
        crimeDetailViewModel.loadCrime(crimeId)
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        crimeDetailViewModel.crimeLiveData.observe(
            viewLifecycleOwner,
            Observer { crime ->
                crime?.let {
                    this.crime = crime
                    updateUI()
                }
            }
        )
    }

    private fun updateUI() {
        titleField.setText(crime.title)
        dataButton.text = crime.date.toString()

        //solvedCheckBox.isChecked = crime.isSolved
        solvedCheckBox.apply {
            isChecked = crime.isSolved
            jumpDrawablesToCurrentState() //跳过CheckBox 当前的勾选动画
        }
    }

  1. CrimeFragment.onCreate()中调用 crimeDetailViewModel.loadCrime(crimeId), 触发 VieMode中 crimeIdLiveData = MutableLiveData() 的更新,
    进一步触发了ViewModel中的crimeLiveData: LiveData<Crime?> 的更新

  2. CrimeFragment观察了ViewModel中的crimeLiveData: LiveData<Crime?> 的变化

 crimeDetailViewModel.crimeLiveData.observe(
            viewLifecycleOwner,
            Observer { crime ->
                crime?.let {
                    this.crime = crime
                    updateUI()
                }
            }
        )

crimeLiveData有变化,updateUI()就被调用进行UI更新。

更新 Crime

第一步:CrimeFrament在生命周期函数 onStop() 调用 ViewMode 的saveCrime(crime)方法

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

生命周期函数 Fragment.onStop():

  • 在用户离开,如:按回退键,
  • 切换任务(比如按了HOME键,使用概览屏),
  • 甚至是因内存不足进程被杀
    都保证用户编辑的数据被保存

第二步: ViewMode 使用仓储CrimeRepository的 updateCrime(crime: Crime)

class CrimeRepository private constructor(context: Context) {
    ...
    private val executor = Executors.newSingleThreadExecutor()
    ...
    fun getCrime(id: UUID): LiveData<Crime?> = crimeDao.getCrime(id)

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

executor = Executors.newSingleThreadExecutor() 能使得数据库操作在后台新线程上执行,防止堵塞主线程

第三步:CrimeDao定义更新函数

@Dao
interface CrimeDao {
    @Query("select * from crime")
    fun getCrimes(): LiveData<List<Crime>>

    @Query("select * from crime where id=(:id)")
    fun getCrime(id: UUID): LiveData<Crime?>

    @Update
    fun updateCrime(crime: Crime)

    @Insert
    fun addCrime(crime: Crime)
}

推荐阅读