Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

Learn how to replace fragments, transfer data between them, and use LiveData Transformation to load immutable data in response to UI state changes.

A single Activity with many fragments

This is an architecture with only one activity + multiple fragments. The activity is responsible for responding to user events and switching between fragments.

In theory, a Fragment is a self-contained component that can be assembled. To maintain the Fragment’s independence, you should define callback interfaces inside the Fragment to do whatever it shouldn’t do for its hosted activity. Tasks such as managing scheduled fragments and determining layout dependencies are left to managed activities by implementing callback interfaces. Instead of taking the Actvity directly from the fragment to complete it.

  • Fragment callback interface
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)
    }
}
Copy the code

CrimeListFragment can now call its function that hosts the activity. As for who is hosting the activity is not important, as long as it implements CrimeListFragment. Callbacks interface, CrimeListFragment work are all the same.

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) } } ... }Copy the code
  • Replace the fragments
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()
    }
}
Copy the code

If you press the back key, you’re out of the application. I’m going to add a fallback stack, and I can give it a name, but it’s optional.

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

Second, the fragments of argument

Each Fragment instance can come with a Fragment argument Bundle object. Fragment data transfer.

  • Append argument to 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 }
        }
    }
...
}
Copy the code

Note that activities and fragments do not and cannot remain independent of each other at the same time. MainActivity must know the internal details of a CrimeFragment in order to host the fragment, but the fragment does not need to know the details of its hosting activity, thus ensuring that the fragment is independent.

  • For 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") } ... }Copy the code

Third, use LiveData data conversion

You need to add the CrimeDetailViewModel to manage database queries and data updates after different queries.

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 } }Copy the code

CrimeIdLiveData holds the ID of the Crime object that the CrimeFragment is currently displaying (or will display). The Crime ID was not set when the CrimeDetailViewModel was first created. But in the end, can call CrimeDetailViewModel CrimeFragment loadCrime (UUID) to let the ViewModel know load which crime object.

In general, viewModels should never expose MutableLiveData.

LiveData Transformation is a workaround to set the trigger and feedback relationship between two LiveData objects. A data conversion function takes two parameters: a LiveData object that acts as a trigger and a mappingfunction that returns the LiveData object. The data transformation function returns a data transformation result — essentially a new LiveData object. Each time the trigger LiveData has a new value set, the value of the new LiveData object returned by the data conversion function is updated.

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() } } ... }Copy the code

The above jumpDrawablesToCurrentState () function is to skip the checkbox checked animation.

Update the database

The crime data can only be stored in the database, and the crime details page can modify the crime data state, so after modification.

By default, all queries must be executed on a separate thread. Room supports Kotlin coroutines, so we need to annotate the query with the suspend decorator. It is then called from a coroutine or other pending function.

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

    @Update
    fun updateCrime(crime: Crime)
}
Copy the code
  • Using executor

The book in order to create a new thread calls database inserts, used to is the Executor of technology, the Executor is introduced may refer to: developer.android.com/reference/k…

And about the Android thread pool a: blog.csdn.net/wangbaochu/…

Of course, you can also use coroutines directly, so that you don’t manipulate the database directly from the UI thread, which would cause ANR.

First according to the code in the book to hit a familiar familiar. Then you can do some exercises on your own. Since I originally used coroutines to insert my own CRIME data into the database in order to pre-insert data, this exercise will just update the database and follow the example code in the book.

. 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) } }Copy the code

The newSingleThreadExecutor() function returns an executor instance pointing to the new thread. Any work performed with this Executor instance occurs on the background process to which it points.

  • Database write and Fragment life cycle

Add functions to the crimeDetailViewModel.kt file to write data to the database.

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

Then override the CrimeFragment onStop() method to save data as long as the page is no longer visible.

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

Why use Fragment Argument

Fragment prevents data reconstruction loss when device configurations change.

Because the activity rebuilds, and the fragment rebuilds, new fragments are still added to the new activity, but the fragment rebuilds by default by calling the fragment constructor with no arguments. So, the new fragment loses the parameter data. However, the Fragment Argument can be saved even after the Fragment is destroyed. When the Fragment is rebuilt, the saved Argument can be assigned to the new Fragment. Even using onSaveInstanceState(Bundle) to prevent data loss is costly to maintain.

Vi. In-depth study: Navigation architecture component library

Developer. The android. Google. Cn/guide/navig…

You can try modifying the code with the above address. After completing the entire book, the code will be updated to Github.

Seven, challenge exercise: to achieve efficient RecyclerView refresh

Since modifying crime details only modifies one item, and returning to the list page refreshes the whole list, the efficiency is too low. Now, it is required to improve the refresh efficiency of RecyclerView, and only update the currently modified record when returning to the list page every time.

At this time, need to change the Adapter, inherited from the previous RecyclerView. Adapter to inherit androidx. RecyclerView. Widget. The ListAdapter < Crime, CrimeHolder >.

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

The ListAdapter is a RecyclerView. The Adapter can find the difference between the old and new data to support the RecyclerView, and then tell it to only redraw the changed data. The comparison between old and new data is done on background threads, so it doesn’t slow down UI responses.

Use androidx ListAdapter. Recyclerview. Widget. DiffUtil to decide which part of the data has changed. Implement the DiffUtil.ItemCallback callback function.

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()
        }
    }
Copy the code

In addition, you need to update the CrimeListFragment and submit the updated crime list to the RecyclerView adapter. Call the ListAdapter. SubmitList (MutableList? The LiveData function submits a new list or configures LiveData and watches the data change.

The ListAdapter official introduction: developer.android.com/reference/a…

Eight, other

CriminalIntent project Demo

Github.com/visiongem/A…


🌈 follow me ac~ ❤️

Public account: Ni K Ni K