Search code examples
androidnavigationviewandroid-navigationview

How to edit a navigation menu item title while the DrawerLayout is open on Android


On Android, when a NavigationView is created in a DrawerLayout, the title of individual MenuItems can be modified by 1) getting handles for the MenuItems in the main activity:

// Get a handle for the navigation view.
NavigationView navigationView = findViewById(R.id.navigationview);

// Get handles for the navigation menu items.
final Menu navigationMenu = navigationView.getMenu();
final MenuItem navigationFirstMenuItem = navigationMenu.getItem(0);
final MenuItem navigationSecondMenuItem = navigationMenu.getItem(1);
final MenuItem navigationThirdMenuItem = navigationMenu.getItem(2);

and 2) modifying the titles of the menu items when the drawer is opened using a DrawerListener:

// The drawer listener is used to update the navigation menu.
drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
    @Override
    public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
    }

    @Override
    public void onDrawerOpened(@NonNull View drawerView) {
    }

    @Override
    public void onDrawerClosed(@NonNull View drawerView) {
    }

    @Override
    public void onDrawerStateChanged(int newState) {
        if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) {  // A drawer is opening or closing.
            // Update the title of the menu items.
            navigationFirstMenuItem.setTitle(getString(R.string.newString));
            navigationSecondMenuItem.setEnabled(false);
            navigationThirdMenuItem.setTitle(getString(R.string.title) + " - " + counterInt);
            }
        }
    });

However, once the drawer is open, attempting to change the title of one of the menu items from a part of the main activity running on a different thread results in the following error, which crashes the app:

W/System.err: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

For example, this error can be produced by attempting to update a navigation menu item from inside WebViewClient.shouldInterceptRequest() as follows:

// Get a handle for the WebView.
WebView webView = findViewById(R.id.webview);

// Set a WebViewClient.
webView.setWebViewClient(new WebViewClient() {
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url){
        navigationFirstMenuItem.setTitle(getString(R.string.newString));
        navigationSecondMenuItem.setEnabled(false);
        navigationThirdMenuItem.setTitle(getString(R.string.title) + " - " + counterInt);
    }
}

The question is how to update a menu item, for example, having a counter that increments in one of the titles, while the navigation drawer is open and the event that needs to update the menu item is running on a different thread.


Solution

  • As suggested by @MikeM., the solution to the problem is to use Activity.runOnUiThread() to update the navigation from the same thread that created it. Specifically, the following code works for me.

    // Get a handle for the activity.  This is used to update the menu item titles while the navigation menu is open.
    Activity activity = this;
    
    // Get a handle for the WebView.
    WebView webView = findViewById(R.id.webview);
    
    // Set a WebViewClient.
    webView.setWebViewClient(new WebViewClient() {
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, String url){
            // Update the menu items' titles from the UI thread.
            activity.runOnUiThread(() -> {
                navigationFirstMenuItem.setTitle(getString(R.string.newString));
                navigationSecondMenuItem.setEnabled(false);
                navigationThirdMenuItem.setTitle(getString(R.string.title) + " - " + counterInt);
            });
        }
    }
    

    The key to understanding this problem is that the navigation view is created by the UI thread. That seems obvious in hindsight, but the error message doesn't specifically state that, and the behavior had initially led me to believe that there was some dedicated DrawerLayout thread that could only be accessed via the DrawerLayout listener.