Search code examples
androidandroid-fragmentsxamarinxamarin.androidmvvmcross

MvxPreferenceFragmentCompat with Up Navigation (back arrow) in ActionBar


I am trying to implement a settings view invoked from a drawer menu.

Settings view is implemented using a Fragment inheriting from MvxPreferenceFragmentCompat. Code is below:

[MvxFragmentPresentation(typeof(MainViewModel), Resource.Id.main_content_frame)]
[Register(nameof(SettingsFragment))]
public class SettingsFragment : MvxPreferenceFragmentCompat<SettingsViewModel> 
{
    public override void OnCreatePreferences(Bundle savedInstanceState, string rootKey)
    {
        SetPreferencesFromResource(Resource.Xml.preferences, rootKey);
    }
}

My preferences.xml is shown below:

<?xml version="1.0" encoding="utf-8" ?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory
        android:title="@string/pref_debug_info_title"
        android:key="pref_key_debug_info">
        <CheckBoxPreference
            android:key="pref_key_provide_debug_info"
            android:title="@string/pref_title_provide_debug_info"
            android:summary="@string/pref_summary_provide_debug_info"
            android:defaultValue="false" />
        <CheckBoxPreference
            android:key="pref_key_provide_debug_info_over_wifi"
            android:title="@string/pref_title_debug_info_over_wifi"
            android:summary="@string/pref_summary_debug_info_over_wifi"
            android:defaultValue="true" />
    </PreferenceCategory>
</PreferenceScreen>

Fragment layout is below:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:local="http://schemas.android.com/apk/res-auto"
    android:id="@+id/fragment_frame"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include
        layout="@layout/include_toolbar_actionbar_addlistitem" />

    <fragment
        android:name="SettingsFragment"
        android:id="@+id/settings_fragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <include
        layout="@layout/include_floatingactionbutton" />

</android.support.design.widget.CoordinatorLayout>

And finally, main activity XML is here:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:fitsSystemWindows="true">

    <FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/main_content_frame"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true" />

    <FrameLayout
      android:id="@+id/navigation_frame"
      android:layout_height="match_parent"
      android:layout_width="wrap_content"
      android:layout_gravity="left|start" />

</android.support.v4.widget.DrawerLayout>

My preferences show up fine but I don't see the AppBar with a title (that I could set) and there is no arrow to navigate back to the Activity. When I press the back "hardware button" once, nothing happens. When I press it the second time, the app "minimizes" and when I bring it back, I am back at the main activity.

I have done this functionality with "regular" Fragments inheriting from MvxFragment but it seems like I am unable to do so with MvxPreferenceFragmentCompat.

Update 1

Based on input from Trevor, I have update fragment_settings.xml and SettingsFragment.cs as shown below:

fragment_settings.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:local="http://schemas.android.com/apk/res-auto"
    android:id="@+id/fragment_frame"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include
        layout="@layout/include_toolbar_actionbar" />

    <!-- Required ViewGroup for PreferenceFragmentCompat -->
    <FrameLayout
        android:id="@android:id/list_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        local:layout_behavior="@string/appbar_scrolling_view_behavior"/>

    <include
        layout="@layout/include_floatingactionbutton" />

</android.support.design.widget.CoordinatorLayout>

SettingsFragment.cs

[MvxFragmentPresentation(typeof(MainViewModel), Resource.Id.main_content_frame)]
[Register(nameof(SettingsFragment))]
public class SettingsFragment : MvxPreferenceFragmentCompat<SettingsViewModel>, View.IOnClickListener
{
    #region properties

    protected Toolbar Toolbar { get; set; }
    protected AppCompatActivity ParentActivity { get; set; }

    #endregion

    #region Fragment lifecycle overrides

    public override void OnCreatePreferences(Bundle savedInstanceState, string rootKey)
    {
        AddPreferencesFromResource(Resource.Xml.preferences);
    }

    public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    {
        var view = base.OnCreateView(inflater, container, savedInstanceState);

        ParentActivity = ((MainActivity)Activity);

        Toolbar = view.FindViewById<Toolbar>(Resource.Id.main_tool_bar);
        ParentActivity.SetSupportActionBar(Toolbar);
        ParentActivity.SupportActionBar.SetDisplayHomeAsUpEnabled(true);
        ParentActivity.SupportActionBar.SetDisplayShowHomeEnabled(true);

        ...

        // TODO: Pull it from a resource
        Toolbar.Title = Resources.GetText(Resource.String.settings_view_title);

        return view;
    }

    #endregion

    #region View.IOnClickListener implementation

    public void OnClick(View v)
    {
        ParentActivity.OnBackPressed(); 
    }

    #endregion
}

But I am still getting null on line ParentActivity.SupportActionBar.SetDisplayHomeAsUpEnabled(true) because my SupportActionBar is null (but Toolbar is not). How come?

Update 2

OK, I got a bit further. I got my AppBar and arrow back and the look and feel is like I have expected it. Still have a problem but more about it below.

What helped? This post.

In short, I needed to declare a custom style for my preference and in it, reference the layout of my preference view. Here are the bit:

Styles.xml

<style name="AppTheme.Base" parent="Theme.AppCompat.Light.NoActionBar">
    ...
    <item name="preferenceTheme">@style/AppTheme.Preference</item>
</style>

<!-- Custom Preference Theme -->
<style name="AppTheme.Preference"
       parent="@style/PreferenceThemeOverlay.v14.Material">
    <item name="preferenceFragmentCompatStyle">
        @style/AppPreferenceFragmentCompatStyle
    </item>
</style>

<!-- Custom Style for PreferenceFragmentCompat -->
<style name="AppPreferenceFragmentCompatStyle"
        parent="@style/PreferenceFragment.Material">
    <item name="android:layout">@layout/fragment_settings</item>
</style>

For me, this has been the missing link between the view declaration in the SettingsFragment and the layout resource.

The only problem that remains is ability to go back from this fragment back to main activity. From a "regular" MvxFragment (that is invoked from a drawer), I use OnClick method of the IOnClickListener to call ParentActivity.OnBackPressed().

SettingsFragment.cs

public class SettingsFragment : MvxPreferenceFragmentCompat<SettingsViewModel>, View.IOnClickListener
{
    ...
    public void OnClick(View v)
    {            
        ParentActivity.OnBackPressed(); 
    }
}

But this does not work from this MvxPreferenceFragmentCompat. Still searching for that answer.

Update 3

Last missing piece of the puzzle was the AddToBackStack declaration atop the class as shown below. Once this was in place, OnBackPressed() worked correctly.

SettingsFragment.cs

[MvxFragmentPresentation(typeof(MainViewModel), Resource.Id.main_content_frame, AddToBackStack = true)]
[Register(nameof(SettingsFragment))]
public class SettingsFragment : MvxPreferenceFragmentCompat<SettingsViewModel>, View.IOnClickListener

Solution

  • You're missing some code to setup the AppCompatActivity.SupportActionBar like you would in any other fragment. Here is how I've solved the problem:

    Here is the SettingsFragment Layout:

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.design.widget.CoordinatorLayout
      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:id="@+id/coordinator"
      android:layout_height="match_parent"
      android:layout_width="match_parent"
      android:fitsSystemWindows="true"
      tools:context=".Activities.MainActivity">
      <include
        layout="@layout/toolbar_actionbar" />
      <!-- Required ViewGroup for PreferenceFragmentCompat -->
      <FrameLayout
        android:id="@android:id/list_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
    </android.support.design.widget.CoordinatorLayout>
    

    Here is the SettingsFragment implementation:

    public class SettingsFragment : MvxPreferenceFragmentCompat<SettingsViewModel>,
        View.IOnClickListener
    {
        private Toolbar _toolbar;
        private View _view;
    
        private MainActivity MainActivity => (MainActivity)Activity;
    
        public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
        {
            // Since the view is created in PreferenceFragmentCompat's OnCreateView we don't use BindingInflate like a typical MvxFragment.
            _view = base.OnCreateView(inflater, container, savedInstanceState);
    
            // TODO: Setup MvvmCross Databinding manually since we didn't use BindingInflate like a typical MvxFragment.
    
            _toolbar = _view.FindViewById<Toolbar>(Resource.Id.toolbar);
            if (_toolbar != null)
            {
                MainActivity.SetSupportActionBar(_toolbar);
                MainActivity.SupportActionBar.SetDisplayHomeAsUpEnabled(true);
                MainActivity.SupportActionBar.SetDisplayShowHomeEnabled(true);
                _toolbar.SetNavigationOnClickListener(this);
    
                // TODO: Bind the Toolbar.Title
            }
            return _view;
        }
    
        public override void OnCreatePreferences(Bundle savedInstanceState, string rootKey)
        {
            AddPreferencesFromResource(Resource.Xml.preferences);
        }
    
        public async void OnClick(View v)
        {
            // Toolbar was clicked
            await ViewModel.CloseCommand.ExecuteAsync().ConfigureAwait(false);
        }
    }