Search code examples
androidandroid-architecture-navigation

Navigate from one activity to another with a NavController


I have two activities one that uses a nav graph and one that doesn't. How can I navigate into a fragment in the nav graph from the activity that doesn't use a NavController?

I'm trying to navigate from the ImportMonsterActivity (after adding the new entity to the db) to the EditMonsterFragment in MainActivity's nav graph.

I think I should be able to create a normal intent and give it some extras to specify where to go in the nav graph, but I haven't found any documentation on this kind of navigation. Everything is either using a deep link from another app or navigating within the nav graph.

If I have to add a deep link to my graph can I do that without using http? I don't want this app to need internet access if possible. I'd like people to be able to just import files they've downloaded or copied to the device.

AndroidManifest.xml

<activity
    android:name=".MainActivity"
    android:label="@string/app_name">
  <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
  </intent-filter>
  <nav-graph android:value="@navigation/mobile_navigation" />
</activity>
<activity
    android:name=".ImportMonsterActivity"
    android:icon="@mipmap/ic_launcher"
    android:label="Import Monster"
    android:launchMode="singleTask"
    android:priority="50">
  <intent-filter>
    <action android:name="android.intent.action.SEND" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="text/plain" />
  </intent-filter>
</activity>

mobile_navigation.xml

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mobile_navigation"
    app:startDestination="@+id/navigation_dashboard">

    <fragment
        android:id="@+id/navigation_search"
        android:name="com.majinnaibu.monstercards.ui.search.SearchFragment"
        android:label="@string/title_search"
        tools:layout="@layout/fragment_search">
        <action
            android:id="@+id/action_navigation_search_to_navigation_monster"
            app:destination="@id/navigation_monster" />
    </fragment>

    <fragment
        android:id="@+id/navigation_dashboard"
        android:name="com.majinnaibu.monstercards.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard">
        <action
            android:id="@+id/action_navigation_dashboard_to_navigation_monster"
            app:destination="@id/navigation_monster" />
    </fragment>

    <fragment
        android:id="@+id/navigation_collections"
        android:name="com.majinnaibu.monstercards.ui.collections.CollectionsFragment"
        android:label="@string/title_collections"
        tools:layout="@layout/fragment_collections">
        <action
            android:id="@+id/action_navigation_collections_to_navigation_monster"
            app:destination="@id/navigation_monster" />
    </fragment>

    <fragment
        android:id="@+id/navigation_library"
        android:name="com.majinnaibu.monstercards.ui.library.LibraryFragment"
        android:label="@string/title_library"
        tools:layout="@layout/fragment_library">
        <action
            android:id="@+id/action_navigation_library_to_navigation_monster"
            app:destination="@id/navigation_monster" />
    </fragment>

    <fragment
        android:id="@+id/navigation_monster"
        android:name="com.majinnaibu.monstercards.ui.monster.MonsterDetailFragment"
        android:label="Monster"
        tools:layout="@layout/fragment_monster">
        <argument
            android:name="monster_id"
            app:argType="string" />
        <action
            android:id="@+id/action_navigation_monster_to_editMonsterFragment"
            app:destination="@id/edit_monster_navigation" />
    </fragment>
    <navigation
        android:id="@+id/edit_monster_navigation"
        app:startDestination="@id/editMonsterFragment">
        <argument
            android:name="monster_id"
            app:argType="string" />

        <fragment
            android:id="@+id/editMonsterFragment"
            android:name="com.majinnaibu.monstercards.ui.editmonster.EditMonsterFragment"
            android:label="Edit Monster"
            tools:layout="@layout/fragment_edit_monster">
            <argument
                android:name="monster_id"
                app:argType="string" />
            <action
                android:id="@+id/action_editMonsterFragment_to_editBasicInfoFragment"
                app:destination="@id/editBasicInfoFragment" />
            <action
                android:id="@+id/action_editMonsterFragment_to_editArmorFragment"
                app:destination="@id/editArmorFragment" />
            <action
                android:id="@+id/action_editMonsterFragment_to_editSpeedFragment"
                app:destination="@id/editSpeedFragment" />
            <action
                android:id="@+id/action_editMonsterFragment_to_editAbilityScoresFragment"
                app:destination="@id/editAbilityScoresFragment" />
            <action
                android:id="@+id/action_editMonsterFragment_to_editSavingThrowsFragment"
                app:destination="@id/editSavingThrowsFragment" />
            <action
                android:id="@+id/action_editMonsterFragment_to_editChallengeRatingFragment"
                app:destination="@id/editChallengeRatingFragment" />
            <action
                android:id="@+id/action_editMonsterFragment_to_editSkillsFragment"
                app:destination="@id/editSkillsFragment" />
            <action
                android:id="@+id/action_editMonsterFragment_to_editLanguagesFragment"
                app:destination="@id/editLanguagesFragment" />
            <action
                android:id="@+id/action_editMonsterFragment_to_editTraitListFragment"
                app:destination="@id/editTraitListFragment" />
            <action
                android:id="@+id/action_editMonsterFragment_to_editStringsFragment"
                app:destination="@id/editStringsFragment" />
        </fragment>
        <fragment
            android:id="@+id/editBasicInfoFragment"
            android:name="com.majinnaibu.monstercards.ui.editmonster.EditBasicInfoFragment"
            android:label="fragment_edit_basic_info"
            tools:layout="@layout/fragment_edit_basic_info" />
        <fragment
            android:id="@+id/editArmorFragment"
            android:name="com.majinnaibu.monstercards.ui.editmonster.EditArmorFragment"
            android:label="fragment_edit_armor"
            tools:layout="@layout/fragment_edit_armor" />
        <fragment
            android:id="@+id/editSpeedFragment"
            android:name="com.majinnaibu.monstercards.ui.editmonster.EditSpeedFragment"
            android:label="fragment_edit_speed"
            tools:layout="@layout/fragment_edit_speed" />
        <fragment
            android:id="@+id/editAbilityScoresFragment"
            android:name="com.majinnaibu.monstercards.ui.editmonster.EditAbilityScoresFragment"
            android:label="EditAbilityScoresFragment" />
        <fragment
            android:id="@+id/editSavingThrowsFragment"
            android:name="com.majinnaibu.monstercards.ui.editmonster.EditSavingThrowsFragment"
            android:label="fragment_edit_saving_throws"
            tools:layout="@layout/fragment_edit_saving_throws" />
        <fragment
            android:id="@+id/editChallengeRatingFragment"
            android:name="com.majinnaibu.monstercards.ui.editmonster.EditChallengeRatingFragment"
            android:label="fragment_edit_challenge_rating"
            tools:layout="@layout/fragment_edit_challenge_rating" />
        <fragment
            android:id="@+id/editSkillsFragment"
            android:name="com.majinnaibu.monstercards.ui.editmonster.EditSkillsFragment"
            android:label="fragment_edit_skills_list"
            tools:layout="@layout/fragment_edit_skills_list">
            <action
                android:id="@+id/action_editSkillsFragment_to_editSkillFragment"
                app:destination="@id/editSkillFragment" />
        </fragment>
        <fragment
            android:id="@+id/editSkillFragment"
            android:name="com.majinnaibu.monstercards.ui.editmonster.EditSkillFragment"
            android:label="fragment_edit_skill"
            tools:layout="@layout/fragment_edit_skill">
            <argument
                android:name="name"
                app:argType="string" />
            <argument
                android:name="abilityScore"
                app:argType="com.majinnaibu.monstercards.data.enums.AbilityScore" />
            <argument
                android:name="proficiency"
                app:argType="com.majinnaibu.monstercards.data.enums.ProficiencyType" />
            <argument
                android:name="advantage"
                app:argType="com.majinnaibu.monstercards.data.enums.AdvantageType" />
        </fragment>
        <fragment
            android:id="@+id/editLanguagesFragment"
            android:name="com.majinnaibu.monstercards.ui.editmonster.EditLanguagesFragment"
            android:label="fragment_edit_languages_list"
            tools:layout="@layout/fragment_edit_languages_list">
            <action
                android:id="@+id/action_editLanguagesFragment_to_editLanguageFragment"
                app:destination="@id/editLanguageFragment" />
        </fragment>
        <fragment
            android:id="@+id/editLanguageFragment"
            android:name="com.majinnaibu.monstercards.ui.editmonster.EditLanguageFragment"
            android:label="fragment_edit_language"
            tools:layout="@layout/fragment_edit_language">
            <argument
                android:name="name"
                app:argType="string" />
            <argument
                android:name="canSpeak"
                app:argType="boolean" />
        </fragment>
        <fragment
            android:id="@+id/editTraitFragment"
            android:name="com.majinnaibu.monstercards.ui.editmonster.EditTraitFragment"
            android:label="EditTraitFragment">
            <argument
                android:name="description"
                app:argType="string" />
            <argument
                android:name="name"
                app:argType="string" />
            <argument
                android:name="traitType"
                app:argType="com.majinnaibu.monstercards.data.enums.TraitType" />
        </fragment>
        <fragment
            android:id="@+id/editTraitListFragment"
            android:name="com.majinnaibu.monstercards.ui.editmonster.EditTraitsFragment"
            android:label="EditTraitListFragment">
            <action
                android:id="@+id/action_editTraitListFragment_to_editTraitFragment"
                app:destination="@id/editTraitFragment" />
            <argument
                android:name="traitType"
                app:argType="com.majinnaibu.monstercards.data.enums.TraitType" />
        </fragment>
        <fragment
            android:id="@+id/editStringsFragment"
            android:name="com.majinnaibu.monstercards.ui.editmonster.EditStringsFragment"
            android:label="EditStringsFragment">
            <action
                android:id="@+id/action_editStringsFragment_to_editStringFragment"
                app:destination="@id/editStringFragment" />
            <argument
                android:name="stringType"
                app:argType="com.majinnaibu.monstercards.data.enums.StringType" />
        </fragment>
        <fragment
            android:id="@+id/editStringFragment"
            android:name="com.majinnaibu.monstercards.ui.editmonster.EditStringFragment"
            android:label="EditStringFragment">
            <argument
                android:name="stringType"
                app:argType="com.majinnaibu.monstercards.data.enums.StringType" />
            <argument
                android:name="value"
                app:argType="string" />
        </fragment>
    </navigation>
</navigation>

This method in ImportMonsterActivity leaves me at in a new instance of the ImportMonsterActivity with no parameters/extras/args.

private void navigateToEditMonster(UUID monsterId) {
    Logger.logUnimplementedFeature(String.format("navigate to editing the monster %s", monsterId));
    NavDeepLinkBuilder builder = new NavDeepLinkBuilder(this);
    Bundle args = new Bundle();
    args.putString("monster_id", monsterId.toString());
    PendingIntent pi = builder.setGraph(R.navigation.mobile_navigation).setDestination(R.id.edit_monster_navigation).setArguments(args).createPendingIntent();
    try {
        pi.send(); // This line is executed
    } catch (PendingIntent.CanceledException e) {
        e.printStackTrace(); // This exception is not thrown
    }
}

Update: I've tried replacing the second activity with a fragment in the nav graph and adding a deep link to open it for share and view actions, but I'm getting build errors unless I give the deep link an app:uri. When I do set the uri I get an app crash when the intent tries to open my activity.

build error with no uri

Execution failed for task ':app:extractDeepLinksDebug'.
> Navigation XML document <deepLink> element must contain a app:uri attribute.

build error with an empty uri

Execution failed for task ':app:extractDeepLinksDebug'.
> java.net.URISyntaxException: Expected authority at index 2: //

mobile_navigation.xml

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mobile_navigation"
    app:startDestination="@+id/navigation_dashboard">

    <!-- unrelated fragments -->
    
    <fragment
        android:id="@+id/navigation_library"
        android:name="com.majinnaibu.monstercards.ui.library.LibraryFragment"
        android:label="@string/title_library"
        tools:layout="@layout/fragment_library">
        <action
            android:id="@+id/action_navigation_library_to_navigation_monster"
            app:destination="@id/navigation_monster" />
    </fragment>

    <fragment
        android:id="@+id/navigation_monster"
        android:name="com.majinnaibu.monstercards.ui.monster.MonsterDetailFragment"
        android:label="Monster"
        tools:layout="@layout/fragment_monster">
        <argument
            android:name="monster_id"
            app:argType="string" />
        <action
            android:id="@+id/action_navigation_monster_to_editMonsterFragment"
            app:destination="@id/edit_monster_navigation" />
    </fragment>
    <navigation
        android:id="@+id/edit_monster_navigation"
        app:startDestination="@id/editMonsterFragment">
        <argument
            android:name="monster_id"
            app:argType="string" />

        <fragment
            android:id="@+id/editMonsterFragment"
            android:name="com.majinnaibu.monstercards.ui.editmonster.EditMonsterFragment"
            android:label="Edit Monster"
            tools:layout="@layout/fragment_edit_monster">
            <argument
                android:name="monster_id"
                app:argType="string" />
            <action
                android:id="@+id/action_editMonsterFragment_to_editBasicInfoFragment"
                app:destination="@id/editBasicInfoFragment" />
            <!-- other actions here to navigate to fragments in this sub graph -->
        </fragment>
        <!-- other fragments here -->
    </navigation>
    <fragment
        android:id="@+id/monsterImportFragment"
        android:name="com.majinnaibu.monstercards.ui.monster.MonsterImportFragment"
        android:label="MonsterImportFragment"
        tools:layout="@layout/fragment_monster">
        <action
            android:id="@+id/action_monsterImportFragment_to_edit_monster_navigation"
            app:destination="@id/edit_monster_navigation" />
        <deepLink
            android:id="@+id/deepLink2"
            app:action="ACTION_VIEW"
            app:mimeType="application/octet-stream" />
    </fragment>
</navigation>

If I replace that deep link with this then the app crashes when loading with a matching action and mimeType.

<deepLink
    android:id="@+id/deepLink2"
    app:action="ACTION_VIEW"
    app:mimeType="application/octet-stream"
    app:uri="app://import-monster" />

Error when setting an app:uri

06-30 13:41:52.004 19299 19299 E AndroidRuntime: Process: com.majinnaibu.monstercards, PID: 19299
06-30 13:41:52.004 19299 19299 E AndroidRuntime: java.lang.RuntimeException: Unable to start activity ComponentInfo{com.majinnaibu.monstercards/com.majinnaibu.monstercards.MainActivity}: android.view.InflateException: Binary XML file line #33 in com.majinnaibu.monstercards:layout/activity_main: Binary XML file line #33 in com.majinnaibu.monstercards:layout/activity_main: Error inflating class androidx.fragment.app.FragmentContainerView
06-30 13:41:52.004 19299 19299 E AndroidRuntime: Caused by: android.view.InflateException: Binary XML file line #33 in com.majinnaibu.monstercards:layout/activity_main: Binary XML file line #33 in com.majinnaibu.monstercards:layout/activity_main: Error inflating class androidx.fragment.app.FragmentContainerView
06-30 13:41:52.004 19299 19299 E AndroidRuntime: Caused by: android.view.InflateException: Binary XML file line #33 in com.majinnaibu.monstercards:layout/activity_main: Error inflating class androidx.fragment.app.FragmentContainerView
06-30 13:41:52.005 19299 19299 E AndroidRuntime:    at com.majinnaibu.monstercards.MainActivity.onCreate(MainActivity.java:34)
06-30 13:41:52.009  1367  2360 W ActivityManager: crash : com.majinnaibu.monstercards,0
06-30 13:41:52.010  1367  2360 W ActivityTaskManager:   Force finishing activity com.majinnaibu.monstercards/.MainActivity
06-30 13:41:52.012  1367  2360 W ActivityTaskManager:   Force finishing activity com.majinnaibu.monstercards/.MainActivity

Solution

  • This is a workaround because I couldn't find the answer. I'm hoping I can find a better solution in time. I'm not going to mark this as the answer because it's a workaround and doesn't answer the question.

    I created a fragment to replace the separate activity, but I was unable to get a deep link to it to work respond to the same intents action=android.intent.action.VIEW and mimeType=text/plain or application/octet-stream Instead I'm processing the intents in MainActivity's onNewIntent(Intent intent) method and if I recognize an import intent I'm doing the navigation to the import fragment there. This lets me use the NavigationController normally to navigate inside the import fragment.

    I still think this is something that should work and I just haven't figured out how, but for anyone else looking to do something similar my only advice is rearchitect your solution to not need this.

    In my manifest I moved the intent filters to my MainActivity and set it's launchMode to single Task. AndroidManifest.xml

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.majinnaibu.monstercards">
        <application
            android:name=".MonsterCardsApplication"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
            <activity
                android:name=".MainActivity"
                android:label="@string/app_name"
                android:launchMode="singleTask">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
                <intent-filter>
                    <action android:name="android.intent.action.SEND" />
    
                    <category android:name="android.intent.category.DEFAULT" />
                    <data android:mimeType="text/plain" />
                </intent-filter>
                <intent-filter>
                    <action android:name="android.intent.action.VIEW" />
                    <action android:name="android.intent.action.EDIT" />
                    <action android:name="android.intent.action.PICK" />
                    <action android:name="android.intent.action.INSERT" />
                    <action android:name="android.intent.action.INSERT_OR_EDIT" />
    
                    <category android:name="android.intent.category.ALTERNATIVE" />
                    <category android:name="android.intent.category.SELECTED_ALTERNATIVE" />
                    <category android:name="android.intent.category.DEFAULT" />
    
                    <data
                        android:mimeType="text/plain"
                        android:scheme="content" />
                    <data
                        android:mimeType="application/octet-stream"
                        android:scheme="content" />
                    <data
                        android:mimeType="text/plain"
                        android:scheme="file" />
                </intent-filter>
    
                <nav-graph android:value="@navigation/mobile_navigation" />
            </activity>
        </application>
    </manifest>
    

    MainActivity.java

    public class MainActivity extends AppCompatActivity {
        @Override
        protected void onNewIntent(Intent intent) {
            super.onNewIntent(intent);
    
            String json = readMonsterJSONFromIntent(intent);
            if (!StringHelper.isNullOrEmpty(json)) {
                NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
                NavController navController = navHostFragment.getNavController();
                NavDirections action = MobileNavigationDirections.actionGlobalMonsterImportFragment(json);
                navController.navigate(action);
            }
        }
    
        private String readMonsterJSONFromIntent(Intent intent) {
            String action = intent.getAction();
            Bundle extras = intent.getExtras();
            String type = intent.getType();
            String json;
            Uri uri = null;
            if ("android.intent.action.SEND".equals(action) && "text/plain".equals(type)) {
                uri = extras.getParcelable("android.intent.extra.STREAM");
            } else if ("android.intent.action.VIEW".equals(action) && ("text/plain".equals(type) || "application/octet-stream".equals(type))) {
                uri = intent.getData();
            } else {
                Logger.logError(String.format("unexpected launch configuration action: %s, type: %s, uri: %s", action, type, uri));
            }
            if (uri == null) {
                return null;
            }
            json = readContentsOfUri(uri);
            if (StringHelper.isNullOrEmpty(json)) {
                return null;
            }
            return json;
        }
    
    private String readContentsOfUri(Uri uri) {
            StringBuilder builder = new StringBuilder();
            try (InputStream inputStream =
                         getContentResolver().openInputStream(uri);
                 BufferedReader reader = new BufferedReader(
                         new InputStreamReader(Objects.requireNonNull(inputStream)))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    builder.append(line);
                }
            } catch (IOException e) {
                Logger.logError("error reading file", e);
                return null;
            }
            return builder.toString();
        }
    

    In my mobile navigation I created a global action to my import fragment and an action from the import fragment to the next fragment I wanted on the stack. I've removed the unrelated bits of my nav xml here.

    mobile_navigation.xml

    <navigation 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/mobile_navigation"
        app:startDestination="@+id/navigation_dashboard">
        <fragment
            android:id="@+id/navigation_library"
            android:name="com.majinnaibu.monstercards.ui.library.LibraryFragment"
            android:label="@string/title_library"
            tools:layout="@layout/fragment_library">
            <action
                android:id="@+id/action_navigation_library_to_navigation_monster"
                app:destination="@id/navigation_monster" />
        </fragment>
        <fragment
            android:id="@+id/navigation_monster"
            android:name="com.majinnaibu.monstercards.ui.monster.MonsterDetailFragment"
            android:label="Monster"
            tools:layout="@layout/fragment_monster">
            <argument
                android:name="monster_id"
                app:argType="string" />
            <action
                android:id="@+id/action_navigation_monster_to_editMonsterFragment"
                app:destination="@id/edit_monster_navigation" />
        </fragment>
        <navigation
            android:id="@+id/edit_monster_navigation"
            app:startDestination="@id/editMonsterFragment">
            <argument
                android:name="monster_id"
                app:argType="string" />
    
            <fragment
                android:id="@+id/editMonsterFragment"
                android:name="com.majinnaibu.monstercards.ui.editmonster.EditMonsterFragment"
                android:label="Edit Monster"
                tools:layout="@layout/fragment_edit_monster">
                <argument
                    android:name="monster_id"
                    app:argType="string" />
            </fragment>
        </navigation>
        <fragment
            android:id="@+id/monsterImportFragment"
            android:name="com.majinnaibu.monstercards.ui.monster.MonsterImportFragment"
            android:label="MonsterImportFragment"
            tools:layout="@layout/fragment_monster">
            <argument
                android:name="json"
                app:argType="string" />
            <action
                android:id="@+id/action_monsterImportFragment_to_navigation_library"
                app:destination="@id/navigation_library"
                app:popUpTo="@id/monsterImportFragment"
                app:popUpToInclusive="true" />
        </fragment>
        <action
            android:id="@+id/action_global_monsterImportFragment"
            app:destination="@id/monsterImportFragment" />
    </navigation>
    

    In my import fragment I'm using this method to setup the back stack I want and navigate to my edit fragment. I'm unaware of another way to remove the current fragment from the stack without knowing what the previous fragment would be and setup the full stack I want without navigating through all the fragments to push them onto the stack.

    MonsterImportFragment.java

    private void navigateToEditMonster(UUID monsterId) {
        NavController navController = Navigation.findNavController(requireView());
        NavDirections action;
        action = MonsterImportFragmentDirections.actionMonsterImportFragmentToNavigationLibrary();
        navController.navigate(action);
        action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monsterId.toString());
        navController.navigate(action);
        action = MonsterDetailFragmentDirections.actionNavigationMonsterToEditMonsterFragment(monsterId.toString());
        navController.navigate(action);
    }
    

    Edit: I think unfortunately there just isn't a way to do this. I still think the feature is a totally valid ask. I think it's possible if you create a deep link in the main activity for the thing you want to link to and navigate to that without using the nav controller at all.