1

So I'm creating my first Android App and I'm already using Jetpack Navigation. I also am implementing my own custom layouts with their own custom toolbars. However, I wanted to take advantage of the built in Jetpack Settings screen i.e. the AndroidX Preference library (https://developer.android.com/develop/ui/views/components/settings).

Because I don't know how big my settings are going to get by the end of this project (don't judge me) I wanted to be able to split my settings across multiple screens as it says here (https://developer.android.com/develop/ui/views/components/settings/organize-your-settings).

But the documentation utilizes fragment factories and emphasize implementation of onPreferenceStartFragment() from an activity. However, my app brings users to the Settings screen via Jetpack Navigation so its already hosted by a Fragment and not an Activity and NavControllers/NavigationGraphs are so far super easy and painless to use.

So can I implement "Split your hierarchy into multiple screens" using a NavController and NavigationGraph?

( I'm going to post an answer, which is what I tried, and so far works)

1 Answers1

1

As promised. This is how I did it. I'm posting because an internet search dindn't find anyone already address the question and since I figured it out I thought it might be helpful to others in the future.

Also, I think its probably obvious to more seasoned Android Developers but I'm new so I'm unsure when things work.

So I'm already using Navigation from a mainNavHostFragment located in my single activity's MainActivity.Java file.

And what you have to type to use "Navigation" is so much nicer than using "Fragment Transactions" and "Fragment Factories" so I figured this out so that I could keep using Navigation and avoid using the o.g. Fragment manipulation techniques.

Now when users click the "Settings" icon I provide them, that first mainNavHostFragment navigates the user to a HolderOfSettingsFragment.java class.

In that HolderOfSettingsFragment.java class I have a second NavHostFragment named settingsNavHostFragment which is ...

  1. initialized using getChildFragmentManager instead of getSupportFragmentManager
  2. and initialized to the androidx.fragment.app.FragmentContainerView in its layout.

And in the usual fashion I have a settingsNavController gotten from the settingsNavHostFragment.

That FragmentContainerView uses a navGraph named sub_navigation_from_settings which is different than the one my MainActivity uses. The starting fragment in sub_navigation_from_settings is my root_preferences and its got a [navigation] action to my SubSettingsExampleFragment.

According to the documentation at https://developer.android.com/develop/ui/views/components/settings all you have to do to make a "link" from the root preference screen to an additional screen is list a <Preference/> tag with an app:title entry that will match the <PreferenceCategory> in the "linked" screen as well as app:fragment indicating the fragment class to jump to.

The Preference Fragments themselves are actually pretty boring. They are literally just a class that extends PreferenceFragmentCompat which implements onCreatePreferences whose single action is to apply setPreferencesFromResource to appropriate preference resources located in your XML directory.

According to the same documentation in order to respond to "link" clicks in the root_prefernces you need to implement PreferenceFragmentCompat.OnPreferenceStartFragmentCallback. The documentation says to do this in the Activity but I did it in HolderOfSettingsFragment and it works just fine.

Further, implementing the PreferenceFragmentCompat.OnPreferenceStartFragmentCallback interface means implementing the onPreferenceStartFragment which is called with both a caller and 'pref`. So we do that.

To let the settingsNavHostFragment handle things we just check if pref.getTitle matches a given title and if it does we use the settingsNavController to navigate to the appropriate "Preference Fragment" using the id of the actions in the sub_navigation_from_settings graph.

Its working just fine for me. Yay!

Its possible that down the line this won't work because of something I don't know causing a conflict but until then I'm glad it works.

I hope this helps someone other than me.

Here is all the code if you want to look at that directly.

Here is the holder java class:

package com.example.app;

import ...

import com.example.app.databinding.FragmentSettingsHolderBinding;


public class HolderOfSettingsFragment extends Fragment implements
        PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {

    public static final String TAG = "HolderOfSettingsFragment-";
    FragmentSettingsHolderBinding bindingOfThis;
    NavController settingsNavController;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        bindingOfThis = FragmentSettingsHolderBinding.inflate(inflater, container, false);
        return bindingOfThis.getRoot();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        NavHostFragment settingsNavHostFragment = (NavHostFragment) getChildFragmentManager().findFragmentById(R.id.settingsHolderFragContainer);
        settingsNavController = settingsNavHostFragment.getNavController();

        bindingOfThis.imgReturnIcon.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ((AppCompatActivity) getActivity()).onBackPressed();
            }
        });

    }
    @Override
    public boolean onPreferenceStartFragment(@NonNull PreferenceFragmentCompat caller, @NonNull Preference pref) {
        String stringToCompare = (String) pref.getTitle();
        if ( stringToCompare.equalsIgnoreCase("Other Settings") ) {
            settingsNavController.navigate(R.id.action_settingsFragment_to_subSettingsExampleFragment);
        }
        return true;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        bindingOfThis = null;
    }
}

It has the following layout:

Note that the Android Studio Editor will not render this layout and I believe that's because the Settings resources are pure-er XML stored in the XML resource directory.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".HolderOfSettingsFragment">

    <ImageView
        android:id="@+id/imgReturnIcon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="12dp"
        android:layout_marginTop="12dp"
        android:layout_marginEnd="12dp"
        android:src="?attr/actionModeCloseDrawable"
        app:layout_constraintEnd_toStartOf="@+id/txtSettingsTitle"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/txtSettingsTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="12dp"
        android:text="Settings"
        android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionMode.Title"
        app:layout_constraintBottom_toBottomOf="@+id/imgReturnIcon"
        app:layout_constraintStart_toEndOf="@+id/imgReturnIcon"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/settingsHolderFragContainer"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintTop_toBottomOf="@id/imgReturnIcon"
        android:layout_marginTop="40dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/sub_navigation_from_settings"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

Here is the second NavGraph:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/sub_navigation_from_settings"
    app:startDestination="@id/settingsFragment">

    <fragment
        android:id="@+id/settingsFragment"
        android:name="com.example.app.SettingsFragment"
        android:label="SettingsFragment" >
        <action
            android:id="@+id/action_settingsFragment_to_subSettingsExampleFragment"
            app:destination="@id/subSettingsExampleFragment" />
    </fragment>
    <fragment
        android:id="@+id/subSettingsExampleFragment"
        android:name="com.example.app.SubSettingsExampleFragment"
        android:label="SubSettingsExampleFragment" />
</navigation>

Here is the root settings XML:

<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">

    <PreferenceCategory app:title="Current User">

        <SwitchPreferenceCompat
            app:key="current_user_hide_tb_labels"
            app:title="Hide labels underneath Toolbar Icons?"
            app:defaultValue="false"/>

    </PreferenceCategory>

    <Preference
        app:title="Other Settings"
        app:summary="Other Settings Example"
        app:fragment="com.example.app.SubSettingsExampleFragment" /> 

And finally the separate example settings screen's xml:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
    <PreferenceCategory app:title="Other Settings">

        <SwitchPreferenceCompat
            app:key="do_you_like"
            app:title="Do you like me?"
            app:defaultValue="false"/>

    </PreferenceCategory>
</PreferenceScreen>