Single Activity: Fragment Boss

In GeoQuiz, you had one activity (MainActivity) start another activity (CheatActivity). In CriminalIntent, you are instead going to use a single activity architecture. An app that uses single activity architecture has one activity and multiple fragments. The activity’s job is to swap fragments in and out in response to user events.

To implement the navigation from CrimeListFragment to CrimeFragment in response to the user pressing on a crime in the list, you might think to initiate a fragment transaction on the hosting activity’s fragment manager in CrimeListFragment’s CrimeHolder.onClick(View). This onClick(View) would get MainActivity’s FragmentManager and commit a fragment transaction that replaces CrimeListFragment with CrimeFragment.

The code in your CrimeListFragment.CrimeHolder would look like this:

    fun onClick(view: View) {
        val fragment = CrimeFragment.newInstance(crime.id)
        val fm = activity.supportFragmentManager
        fm.beginTransaction()
          .replace(R.id.fragment_container, fragment)
          .commit()
    }

This works, but it is not how stylish Android programmers do things. Fragments are intended to be standalone, composable units. If you write a fragment that adds fragments to the activity’s FragmentManager, then that fragment is making assumptions about how the hosting activity works, and your fragment is no longer a standalone, composable unit.

For example, in the code above, CrimeListFragment adds a CrimeFragment to MainActivity and assumes that MainActivity has a fragment_container in its layout. This is business that should be handled by CrimeListFragment’s hosting activity instead of CrimeListFragment.

To maintain the independence of your fragments, you will delegate work back to the hosting activity by defining callback interfaces in your fragments. The hosting activities will implement these interfaces to perform fragment-bossing duties and layout-dependent behavior.

Fragment callback interfaces

To delegate functionality back to the hosting activity, a fragment typically defines a custom callback interface named Callbacks. This interface defines work that the fragment needs done by its boss, the hosting activity. Any activity that will host the fragment must implement this interface.

With a callback interface, a fragment is able to call functions on its hosting activity without having to know anything about which activity is hosting it.

Use a callback interface to delegate on-click events from CrimeListFragment back to its hosting activity. First, open CrimeListFragment and define a Callbacks interface with a single callback function. Add a callbacks property to hold an object that implements Callbacks. Override onAttach(Context) and onDetach() to set and unset the callbacks property.

Listing 12.1  Adding a callback interface (CrimeListFragment.kt)

class CrimeListFragment : Fragment() {

    /**
     * Required interface for hosting activities
     */
    interface Callbacks {
        fun onCrimeSelected(crimeId: UUID)
    }

    private var callbacks: Callbacks? = null

    private lateinit var crimeRecyclerView: RecyclerView
    private var adapter: CrimeAdapter = CrimeAdapter(emptyList())
    private val crimeListViewModel: CrimeListViewModel by lazy {
        ViewModelProviders.of(this).get(CrimeListViewModel::class.java)
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        callbacks = context as Callbacks?
    }

    override fun onCreateView(
        ...
    ): View? {
        ...
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
    }

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

The Fragment.onAttach(Context) lifecycle function is called when a fragment is attached to an activity. Here you stash the Context argument passed to onAttach(…) in your callbacks property. Since CrimeListFragment is hosted in an activity, the Context object passed to onAttach(…) is the activity instance hosting the fragment.

Remember, Activity is a subclass of Context, so onAttach(…) passes a Context as a parameter, which is more flexible. Ensure that you use the onAttach(Context) signature for onAttach(…) and not the deprecated onAttach(Activity) function, which may be removed in future versions of the API.

Similarly, you set the variable to null in the corresponding waning lifecycle function, Fragment.onDetach(). You set the variable to null here because afterward you cannot access the activity or count on the activity continuing to exist.

Note that CrimeListFragment performs an unchecked cast of its activity to CrimeListFragment.Callbacks. This means that the hosting activity must implement CrimeListFragment.Callbacks. That is not a bad dependency to have, but it is important to document it.

Now CrimeListFragment has a way to call functions on its hosting activity. It does not matter which activity is doing the hosting. As long as the activity implements CrimeListFragment.Callbacks, everything in CrimeListFragment can work the same.

Next, update the click listener for individual items in the crime list so that pressing a crime notifies the hosting activity via the Callbacks interface. Call onCrimeSelected(Crime) in CrimeHolder.onClick(View).

Listing 12.2  Calling all callbacks! (CrimeListFragment.kt)

class CrimeListFragment : Fragment() {
    ...
    private inner class CrimeHolder(view: View)
        : RecyclerView.ViewHolder(view), View.OnClickListener {
        ...
        fun bind(crime: Crime) {
            ...
        }

        override fun onClick(v: View?) {
            Toast.makeText(context, "${crime.title} clicked!", Toast.LENGTH_SHORT)
                .show()
            callbacks?.onCrimeSelected(crime.id)
        }
    }
    ...
}

Finally, update MainActivity to implement CrimeListFragment.Callbacks. Log a debug statement in onCrimeSelected(UUID) for now.

Listing 12.3  Implementing callbacks (MainActivity.kt)

private const val TAG = "MainActivity"
class MainActivity : AppCompatActivity(),
    CrimeListFragment.Callbacks {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }

    override fun onCrimeSelected(crimeId: UUID) {
        Log.d(TAG, "MainActivity.onCrimeSelected: $crimeId")
    }
}

Run CriminalIntent. Search or filter Logcat to view MainActivity’s log statements. Each time you press a crime in the list you should see a log statement indicating the click event was propagated from CrimeListFragment to MainActivity through Callbacks.onCrimeSelected(UUID).

Replacing a fragment

Now that your callback interface is wired up correctly, update MainActivity’s onCrimeSelected(UUID) to swap out the CrimeListFragment with an instance of CrimeFragment when the user presses a crime in CrimeListFragment’s list. For now, ignore the crime ID passed to the callback.

Listing 12.4  Replacing CrimeListFragment with CrimeFragment (MainActivity.kt)

class MainActivity : AppCompatActivity(),
    CrimeListFragment.Callbacks {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }

    override fun onCrimeSelected(crimeId: UUID) {
        Log.d(TAG, "MainActivity.onCrimeSelected: $crimeId")
        val fragment = CrimeFragment()
        supportFragmentManager
            .beginTransaction()
            .replace(R.id.fragment_container, fragment)
            .commit()
    }
}

FragmentTransaction.replace(Int, Fragment) replaces the fragment hosted in the activity (in the container with the integer resource ID specified) with the new fragment provided. If a fragment is not already hosted in the container specified, the new fragment is added, just as if you had called FragmentTransaction.add(Int, fragment).

Run CriminalIntent. Press a crime in the list. You should see the crime detail screen appear (Figure 12.2).

Figure 12.2  A blank CrimeFragment

A blank CrimeFragment

For now, the crime detail screen is empty, because you have not told CrimeFragment which Crime to display. You will populate the detail screen shortly. But first, you need to file down a remaining sharp edge in your navigation implementation.

Press the Back button. This dismisses MainActivity. This is because the only item in your app’s back stack is the MainActivity instance that was launched when you launched the app.

Users will expect that pressing the Back button from the crime detail screen will bring them back to the crime list. To implement this behavior, add the replace transaction to the back stack.

Listing 12.5  Adding fragment transaction to the back stack (MainActivity.kt)

class MainActivity : AppCompatActivity(),
    CrimeListFragment.Callbacks {
    ...
    override fun onCrimeSelected(crimeId: UUID) {
        val fragment = CrimeFragment()
        supportFragmentManager
            .beginTransaction()
            .replace(R.id.fragment_container, fragment)
            .addToBackStack(null)
            .commit()
    }
}

When you add a transaction to the back stack, this means that when the user presses the Back button the transaction will be reversed. So, in this case, CrimeFragment will be replaced with CrimeListFragment.

You can name the back stack state you are adding by passing a String to FragmentTransaction.addToBackStack(String). Doing so is optional and, since you do not care about the name in this implementation, you pass null.

Run your app. Select a crime from the list to launch CrimeFragment. Press the Back button to go back to CrimeListFragment. Enjoy the simple pleasure of your app’s navigation matching what users will expect.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.235.196