《Android 编程权威指南》学习笔记 : 第12章 Fragment Navigation
第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
因为,必须保证在 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 当前的勾选动画
}
}
-
CrimeFragment.onCreate()中调用 crimeDetailViewModel.loadCrime(crimeId), 触发 VieMode中 crimeIdLiveData = MutableLiveData
() 的更新,
进一步触发了ViewModel中的crimeLiveData: LiveData<Crime?> 的更新 -
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)
}