safe-args to pass Bundles safely between Activities/Fragments

Previously there was no way or guaranteeing that the data handed across in Bundles was what was expected. The safe-args plugin seeks to address this:

dependencies {
  ...
classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"

   // NOTE: Do not place your application dependencies here; they belong
   // in the individual module build.gradle files
}
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-kapt'

// Add the safe-args plugin after any others:
apply plugin: 'androidx.navigation.safeargs'

android {
    compileSdkVersion 28
    ...

In the Navigation component select the Fragment which is passing on arguments and add Arguments using the + symbol next to the Arguments section of the GUI. Give each a name and specify type if necessary.

import ...

class TitleFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val binding: FragmentTitleBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_title, container, false)
        binding.playButton.setOnClickListener {v: View ->
 //               v.findNavController().navigate(R.id.action_titleFragment_to_nextFragment) // Before safe-args
          v.findNavController().navigate(TitleFragmentDirections.actionTitleFragmentToNextFragment(firstParameter, secondParameter)) // With safe-args. Creates a class *Directions based on Fragment name with associated actions which take the parameters to be passed on
        }
        setHasOptionsMenu(true)
        return binding.root
    }

    override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater?.inflate(R.menu.overflow_menu, menu)
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        return NavigationUI.onNavDestinationSelected(item!!, view!!.findNavController())
                || super.onOptionsItemSelected(item)
    }
}

In the receiving fragment:

import ...

class NextFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val binding: FragmentTitleBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_next, container, false)
        val args = NextFragmentArgs.fromBundle(arguments!!) // Get arguments.
        Toast.makeText(context, "First Parameter: ${args.firstParameter}, Second Parameter: ${args.secondParameter}", Toast.LENGTH_SHORT).show() // Use the Bundled parameters
        return binding.root
    }
}

 

Create Fragments

Add fragments to your app.

Create a new class for our Fragment:

public class AppFragment extends Fragment {

    public AppFragment() { // Empty constructor
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { // Essential
        View rootView = inflater.inflate(R.layout.our_fragment, container, false); // Use a custom layout file (see below)

        ImageView imageView = rootView.findViewById(R.id.ourImageView); // Here we find a View within the layout

        imageView.setImageResource(R.drawable.anImage); // And allocate an image to it

        return rootView;
    }
}

Our layout file for the Fragment, our_fragment.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/ourImageView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:contentDescription="An Image"
        tools:srcCompat="@drawable/anImage" /> // Placeholder image
</LinearLayout>

Create an instance of our Fragment in the Activity and use FragmentManager to add it to the layout for the Activity:

        AppFragment aFragment = new AppFragment(); // New instance of our custom Fragment

        FragmentManager fragmentManager = getSupportFragmentManager();

        fragmentManager.beginTransaction()
                .add(R.id.fragment_container, aFragment) // Adds the new fragment to an element of the Activity layout (see below)
                .commit();

Our layout file for the Activity, activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.android.app.MainActivity">

    <LinearLayout
        android:id="@+id/main_activity_linear_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <FrameLayout android:id="@+id/fragment_container" // View that will contain the Fragment
        android:layout_width="match_parent"
        android:layout_height="180dp"
        android:scaleType="centerInside"/>

    </LinearLayout>
</ScrollView>

onSaveInstanceState

How to persist data across changes in state such as device rotation. The onSavedInstanceState function stores information in a Bundle and is limited in the data it can contain (small amounts of data which can be easily serialised and de-serialised). It does not persist on app closure. **Try to keep the contents to considerably less than 100KB as this resides in RAM**

This example overrides the onPause and onSaveInstanceState functions to append an update to the screen when these events occur. When the screen is rotated the onSaveInstanceState function is called – it takes the contents of the screen (in particular the tv_lifecycle_events_display TextView) and stores it in the savedInstanceState Bundle. When onCreate is called after screen rotation, it checks for the LIFECYCLE_CALLBACKS_TEXT_KEY in the Bundle and, if present, writes the data back to the screen.

Add the following to your Activity file:

private static final String LIFECYCLE_CALLBACKS_TEXT_KEY = "callbacks"; // Key string constant for the data you will store
private static final String ON_PAUSE = "onPause"; // The text that will be displayed onscreen when the onPause state occurs
private static final String ON_SAVE_INSTANCE_STATE = "onSaveInstanceState"; // The text that will be displayed onscreen when the onSaveInstanceState state occurs

    protected void onCreate(Bundle savedInstanceState) { // Standard onCreate override
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        private TextView mLifecycleDisplay = findViewById(R.id.tv_lifecycle_events_display); // TextView the data will be printed to


        /*
         * If savedInstanceState is not null, that means our Activity is not being started for the
         * first time. Even if the savedInstanceState is not null, it is smart to check if the
         * bundle contains the key we are looking for. In our case, the key we are looking for maps
         * to the contents of the TextView that displays our list of callbacks. If the bundle
         * contains that key, we set the contents of the TextView accordingly.
         */
        if (savedInstanceState != null) {
            if (savedInstanceState.containsKey(LIFECYCLE_CALLBACKS_TEXT_KEY)) { // If key is present...
                String allPreviousLifecycleCallbacks = savedInstanceState.getString(LIFECYCLE_CALLBACKS_TEXT_KEY);
                mLifecycleDisplay.setText(allPreviousLifecycleCallbacks); // ... display all of the previous contents to the TextView
            }
        }


    @Override
    protected void onPause() {
        super.onPause();
        mLifecycleDisplay.append(ON_PAUSE + "\n"); // Displays the event onscreen
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) { // Override onSaveInstanceState
        super.onSaveInstanceState(outState); // Call super class - ALWAYS DO THIS
        mLifecycleDisplay.append(ON_SAVE_INSTANCE_STATE + "\n"); // Displays the event onscreen
        String lifecycleDisplayTextViewContents = mLifecycleDisplay.getText().toString(); // Gets all of the content from the TextView
        outState.putString(LIFECYCLE_CALLBACKS_TEXT_KEY, lifecycleDisplayTextViewContents); // Puts TextView content into savedInstanceState Bundle
    }

}