19

I've been using the new Navigation Component since shortly after it has been announced at Google I/O, and also started to embrace the single-activity as much as possible.

The Single Activity allowed me to share ViewModels between view for an awesome experience and I really don't want to go back to multi-activity if I'm not forced to.

But there's something that gets in the way: AppBar / Themes (status bar) to the single activity concept.

This is part of the design I'm working in:

AppBar design in different sections

As you can see there are different requirments for how the Actionbar / status bar should look.

  1. It's a simple drawer with standard actionbar
  2. Classic detail with image going under the translucent status bar, supposed to use CollapsingToolbarLayout to turn into a standard actionbar when scrolling up
  3. In this case it is non-standard actionbar, I'd call it a "floating toolbar" cause it doesn't expand to the full with of the screen and contains an already expanded SearchView / EditText
  4. Fairly standard AppBar with tabs

List of issues that arise from leaving the single activity:

  • can't share ViewModels between activities
  • complex navigations which re-use parts already defined in another activity navigation graph have to be duplicated / moved into a dedicated activity
  • back navigation "re-construction" doesn't work between activities

Those are issues I want to avoid if possible, but how do you guys manage these kind of situation on a single-activity with navigation component. Any idea?

Cheticamp
  • 61,413
  • 10
  • 78
  • 131
Daniele Segato
  • 12,314
  • 6
  • 62
  • 88

4 Answers4

2

As mentioned here, the developer document said

Adding the top app bar to your activity works well when the app bar’s layout is similar for each destination in your app. If, however, your top app bar changes substantially across destinations, then consider removing the top app bar from your activity and defining it in each destination fragment, instead.

Dilanka Laksiri
  • 408
  • 3
  • 12
1

I was also thinking the same but never got time to do some experiment. So it's not a solution, it's an experiment, where I want to replace a view with another, here, the toolbar with a toolbar that contains an ImageView.

So I created a new Application using "Basic Activity" template. Then created two destinations within the graph, Home and destination. And lastly, created another layout for Toolbar:

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="?actionBarSize">

    <ImageView
        android:id="@+id/image_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:srcCompat="@mipmap/ic_launcher_round" />

</androidx.appcompat.widget.Toolbar>

The activity_main.xml has:

<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appbar_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </com.google.android.material.appbar.AppBarLayout>

    ...

And then within Activity, of-course depends on the setup, but let's say that I want to setup an support-actionbar with toolbar:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Toolbar toolbar = findViewById(R.id.toolbar);
    Toolbar toolbar2 = (Toolbar) getLayoutInflater().inflate(R.layout.destination_toolbar, null);

    AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);

    navController = Navigation.findNavController(this, R.id.nav_host_fragment);
    appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph())
            .build();

    navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
        switch (destination.getId()) {
            case R.id.homeFragment:
                appBarLayout.removeAllViews();
                appBarLayout.addView(toolbar);
                setSupportActionBar(toolbar);
                toolbar.setTitle("Home Fragment");
                NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
                break;
            case R.id.destinationFragment:
                appBarLayout.removeAllViews();
                appBarLayout.addView(toolbar2);
                setSupportActionBar(toolbar2);
                toolbar2.setTitle("");
                NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
                break;
        }
    });
}

And thus, this works, making it somewhat ugly as destination grows and new Toolbar/any other view is being added.

P.S. As I told earlier, this is just an experiment, if anyone has a better solution, please do post a new answer.

Rajarshi
  • 2,419
  • 3
  • 23
  • 36
0

Disclaimer

Based of @Rajarshi original experiment, I made a working solution for this problem. I'm not sure is the most elegant, or if there are better ways. But after hours of research and investigation, this is the best solution I found.

Solution

Inflate the toolbars separately and store their references so they are not picked by the garbage collector. Then load each on demand in your main AppBarLayout inside a custom OnDestinationChangedListener defined for your navController

Example

Here's an example I've written in Kotlin.

On your activity.xml layout, define an AppBarLayout that is empty.

layout/activity.xml

<androidx.coordinatorlayout.widget.CoordinatorLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"

    ...

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay" />
    ...

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Define the toolbars that your app needs to have in separate layout files.

layout/toolbar_defaul.xml

<com.google.android.material.appbar.MaterialToolbar 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"

    android:id="@+id/default_toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    app:menu="@menu/menu_default"
    app:popupTheme="@style/AppTheme.PopupOverlay" />

layout/toolbar2.xml

<com.google.android.material.appbar.MaterialToolbar 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"

    android:id="@+id/toolbar2"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    app:menu="@menu/menu2"
    app:popupTheme="@style/AppTheme.PopupOverlay" />

In your main (and only) activity, declare AppBar related components as class properties, so that they are not picked up by the garbage collector.

Activity.kt

class Activity : AppCompatActivity() {

    private lateinit var appBarConfiguration: AppBarConfiguration
    private lateinit var appBarLayout: AppBarLayout

    private lateinit var defaultToolbar: MaterialToolbar
    private lateinit var toolbar2: MaterialToolbar

    ...

And finally, in the onCreate method, define a OnDestinationChangedListener for the navController. Use it to load on demand each toolbar.

Activity.kt

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

        setContentView(R.layout.activity_ryvod)

        // Set up AppBar
        appBarLayout = findViewById(R.id.appbar)
        appBarConfiguration = AppBarConfiguration(setOf(R.id.StartFragment))

        defaultToolbar = layoutInflater.inflate(R.layout.toolbar_default, appBarLayout, false) as MaterialToolbar
        toolbar2 = layoutInflater.inflate(R.layout.toolbar2, appBarLayout, false) as MaterialToolbar

        val host =
            supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment?
                ?: return

        val navController = host.navController
        navController.addOnDestinationChangedListener { _, destination, _ ->
            when (destination.id) {
                R.id.locationPickerFragment -> {
                    appBarLayout.removeAllViews()
                    appBarLayout.addView(toolbar2)
                    setSupportActionBar(toolbar2)

                }
                else -> {
                    appBarLayout.removeAllViews()
                    appBarLayout.addView(defaultToolbar)
                    setSupportActionBar(defaultToolbar)
                }
            }
            setupActionBarWithNavController(navController, appBarConfiguration)
        }
    }

That should do the trick

Lorenzo Petroli
  • 458
  • 6
  • 14
  • thanks for your answer, this kinda centralize a distribuited responsibility, each destination know which is the needed toolbar configuration. Some also have a collapsing toolbar layout and thus it has other stuff to take into account. I think the toolbar should stay in the specific fragment. The solution i'm using currently is to put the toolbar inside the single destination and keep calling setSupportActionBar on the activity when i change between destination, I do so from the fragment, but I could improve it by using some interface I suppose. – Daniele Segato Jan 22 '20 at 14:49
  • I have struggled to make the Up Button to work out of the box with Navigation doing the way you describe. I'd be keen to see how you did it if you had an open repository i could check the code – Lorenzo Petroli Jan 28 '20 at 08:14
  • I don't have a public repository with that. But if I recall it was as simple as setting up each toolbar with `NavigationUI`, I believe it was a method like `setupWithNavController`. You do that when you switch toolbar and it should just work. – Daniele Segato Jan 29 '20 at 08:01
  • And you say you did that inside the Fragment not the activity. Is that right? Which lifecycle method (onCreate, onCreateView, etc)? – Lorenzo Petroli Jan 30 '20 at 00:12
  • In `onCreateView` from the fragment i call a method on the activity (through an interface). It's the activity that handle the navigation setup part. – Daniele Segato Jan 30 '20 at 15:44
0

I confronted this problem a while ago, with similar UX/UI as yours:

  1. Sidenav Navigation Drawer
  2. A "normal" Appbar with back arrow
  3. Translucent Appbar/status bar

My solution was having a different .xml Appbar for each case and using the <include/> tag inside every fragment xml:

<include
        android:id="@+id/include"
        layout="@layout/default_toolbar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

The window config for case 1 and case 2 was the same, but for the translucent Appbar, the window config changed, see case 3.

So I had to do a config change every time the fragment showed up/replaced:

public static void transparentStatusBar(Activity activity, boolean isTransparent, boolean fullscreen) {
    if (isTransparent){
        activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
                View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
        activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

    }else {
        if (fullscreen){
            View decorView = activity.getWindow().getDecorView();
            int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
            decorView.setSystemUiVisibility(uiOptions);
        } else {
            activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
                    | View.SYSTEM_UI_FLAG_VISIBLE);
            activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

        }
    }
}

And then use this method in translucent appbar/status bar fragment's lifecycle:

@Override
public void onResume() {
    super.onResume();
    UtilApp.transparentStatusBar(requireActivity(), true, true);
}

@Override
public void onStop() {
    super.onStop();
    UtilApp.transparentStatusBar(requireActivity(), false, false);
}
colidyre
  • 4,170
  • 12
  • 37
  • 53