Search code examples
javaandroidandroid-fragmentsnavigation-drawerandroid-architecture-navigation

How to open the last fragment opened after closed app and reopen it using Navigation drawer and Navigation Component


Update

Since onSaveInstanceState & onRestoreInstanceState can't be used to store/restore values after closed the app, I tried to use dataStore to solve it, but it dosen't work, here's my trying

DataStoreRepository

@ActivityRetainedScoped
    public static class DataStoreRepository {
        RxDataStore<Preferences> dataStore;

        public static Preferences.Key<Integer> CURRENT_DESTINATION =
                PreferencesKeys.intKey("CURRENT_DESTINATION");

        public final Flowable<Integer> readCurrentDestination;

        @Inject
        public DataStoreRepository(@ApplicationContext Context context) {
            dataStore =
                    new RxPreferenceDataStoreBuilder(Objects.requireNonNull(context), /*name=*/ "settings").build();

            readCurrentDestination = dataStore.data().map(preferences -> {
                if (preferences.get(CURRENT_DESTINATION) != null) {
                    return preferences.get(CURRENT_DESTINATION);
                } else {
                    return R.id.nav_home;
                }

            });
        }


        public void saveCurrentDestination(String keyName, int value){

            CURRENT_DESTINATION = PreferencesKeys.intKey(keyName);
            dataStore.updateDataAsync(prefsIn -> {
                MutablePreferences mutablePreferences = prefsIn.toMutablePreferences();
                Integer currentKey = prefsIn.get(CURRENT_DESTINATION);

                if (currentKey == null) {
                    saveCurrentDestination(keyName,value);
                }

                mutablePreferences.set(CURRENT_DESTINATION,
                        currentKey != null ? value : R.id.nav_home);
                return Single.just(mutablePreferences);
            }).subscribe();

        }

    }

read and save in ViewModel

public final MutableLiveData<Integer> currentDestination = new MutableLiveData<>();


 @Inject
    public PostViewModel(Repository repository, Utils.DataStoreRepository dataStoreRepository) {
        this.repository = repository;
        getAllItemsFromDataBase = repository.localDataSource.getAllItems();
        this.dataStoreRepository = dataStoreRepository;

        dataStoreRepository.readCurrentDestination
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new FlowableSubscriber<Integer>() {
                    @Override
                    public void onSubscribe(@NonNull Subscription s) {
                        s.request(Long.MAX_VALUE);
                    }

                    @Override
                    public void onNext(Integer integer) {

                    }

                    @Override
                    public void onError(Throwable t) {
                        Log.e(TAG, "onError: " + t.getMessage());
                    }

                    @Override
                    public void onComplete() {

                    }
                });

    }

    public void saveCurrentDestination(int currentDestination) {
        dataStoreRepository
                .saveCurrentDestination("CURRENT_DESTINATION", currentDestination);
    }

and finally MainActivity

@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    @SuppressWarnings("unused")
    private AppBarConfiguration mAppBarConfiguration;
    private NavHostFragment navHostFragment;
    private  NavController navController;
    NavGraph navGraph;
    private PostViewModel postViewModel;



    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        postViewModel = new ViewModelProvider(this).get(PostViewModel.class);


        setSupportActionBar(binding.appBarMain.toolbar);
        mAppBarConfiguration = new AppBarConfiguration.Builder(R.id.nav_home, R.id.nav_accessory,
                R.id.nav_arcade, R.id.nav_fashion,
                R.id.nav_food, R.id.nav_heath,
                R.id.nav_lifestyle, R.id.nav_sports, R.id.about)
                .setOpenableLayout(binding.drawerLayout)
                .build();


        navHostFragment = (NavHostFragment)
                getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);

        if(navHostFragment !=null) {
          navController = navHostFragment.getNavController();
        }

        NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
        NavigationUI.setupWithNavController(binding.navView, navController);

        navGraph = navController.getNavInflater().inflate(R.navigation.mobile_navigation);

        postViewModel.currentDestination.observe(this,currentDestination -> {
            Log.d(TAG, "currentDestination: " + currentDestination);
            Toast.makeText(this,"currentDestination" + currentDestination,Toast.LENGTH_SHORT).show();
            navGraph.setStartDestination(currentDestination);
            navController.setGraph(navGraph);
        });

        navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
            Log.d(TAG, "addOnDestinationChangedListener: " + destination.getId());
            postViewModel.saveCurrentDestination(destination.getId());
        });



    }


    @Override
    public boolean onSupportNavigateUp() {
        return NavigationUI.navigateUp(navController, mAppBarConfiguration)
                || super.onSupportNavigateUp();
    }

}

Problem in detail

In this app I have 9 menu items and fragments in navigation drawer, I want to save the last opened fragment in savedInstanceState or datastore and after the user closed the app and re open it again display the last opend fragment, but I don't know which method I'll use

Navigation.findNavController(activity,nav_graph).navigate();

or

binding.navView.setNavigationItemSelectedListener(item -> false);

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout 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/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">

    <include
        android:id="@+id/app_bar_main"
        layout="@layout/app_bar_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true"
        android:background="@color/color_navigation_list_background"
        app:headerLayout="@layout/nav_header_main"
        app:menu="@menu/activity_main_drawer" />

</androidx.drawerlayout.widget.DrawerLayout>

activity_main_drawer.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:showIn="navigation_view">

    <group android:checkableBehavior="single">
        <item
            android:id="@+id/nav_home"
            android:title="@string/home"
            android:icon="@drawable/home"
            />

        <item
            android:id="@+id/nav_accessory"
            android:title="@string/accessory"
            android:icon="@drawable/necklace"
            />
        <item
            android:id="@+id/nav_arcade"
            android:title="@string/arcade"
            android:icon="@drawable/arcade_cabinet"
            />
        <item
            android:id="@+id/nav_fashion"
            android:title="@string/fashion"
            android:icon="@drawable/fashion_trend"
            />
        <item
            android:id="@+id/nav_food"
            android:title="@string/food"
            android:icon="@drawable/hamburger"
            />
        <item
            android:id="@+id/nav_heath"
            android:title="@string/heath"
            android:icon="@drawable/clinic"
            />
        <item
            android:id="@+id/nav_lifestyle"
            android:title="@string/lifestyle"
            android:icon="@drawable/yoga"
            />
        <item
            android:id="@+id/nav_sports"
            android:title="@string/sports"
            android:icon="@drawable/soccer"
            />

        <item
            android:id="@+id/nav_favorites"
            android:title="@string/favorites_posts"
            android:icon="@drawable/ic_favorite"
            />

        <item
            android:id="@+id/about"
            android:title="@string/about"
            android:icon="@drawable/about"
            />

    </group>

</menu>

nav_graph.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/nav_home">

    <fragment
        android:id="@+id/nav_home"
        android:name="com.blogspot.abtallaldigital.ui.HomeFragment"
        android:label="@string/home"
        tools:layout="@layout/fragment_home">
        <action
            android:id="@+id/action_nav_home_to_detailsFragment"
            app:destination="@id/detailsFragment"
            app:popUpTo="@id/nav_home" />
    </fragment>

    <fragment
        android:id="@+id/nav_accessory"
        android:name="com.blogspot.abtallaldigital.ui.AccessoryFragment"
        android:label="@string/accessory"
        tools:layout="@layout/fragment_accessory" >
        <action
            android:id="@+id/action_nav_Accessory_to_detailsFragment"
            app:destination="@id/detailsFragment" />
    </fragment>

    <fragment
        android:id="@+id/nav_arcade"
        android:name="com.blogspot.abtallaldigital.ui.ArcadeFragment"
        android:label="@string/arcade"
        tools:layout="@layout/fragment_arcade" >
        <action
            android:id="@+id/action_nav_Arcade_to_detailsFragment"
            app:destination="@id/detailsFragment" />
    </fragment>

    <fragment
        android:id="@+id/nav_fashion"
        android:name="com.blogspot.abtallaldigital.ui.FashionFragment"
        android:label="@string/fashion"
        tools:layout="@layout/fragment_fashion" >
        <action
            android:id="@+id/action_nav_Fashion_to_detailsFragment"
            app:destination="@id/detailsFragment" />
    </fragment>
    <fragment
        android:id="@+id/nav_food"
        android:name="com.blogspot.abtallaldigital.ui.FoodFragment"
        android:label="@string/food"
        tools:layout="@layout/food_fragment" >
        <action
            android:id="@+id/action_nav_Food_to_detailsFragment"
            app:destination="@id/detailsFragment" />
    </fragment>
    <fragment
        android:id="@+id/nav_heath"
        android:name="com.blogspot.abtallaldigital.ui.HeathFragment"
        android:label="@string/heath"
        tools:layout="@layout/heath_fragment" >
        <action
            android:id="@+id/action_nav_Heath_to_detailsFragment"
            app:destination="@id/detailsFragment" />
    </fragment>
    <fragment
        android:id="@+id/nav_lifestyle"
        android:name="com.blogspot.abtallaldigital.ui.LifestyleFragment"
        android:label="@string/lifestyle"
        tools:layout="@layout/lifestyle_fragment" >
        <action
            android:id="@+id/action_nav_Lifestyle_to_detailsFragment"
            app:destination="@id/detailsFragment" />
    </fragment>
    <fragment
        android:id="@+id/nav_sports"
        android:name="com.blogspot.abtallaldigital.ui.SportsFragment"
        android:label="@string/sports"
        tools:layout="@layout/sports_fragment" >
        <action
            android:id="@+id/action_nav_Sports_to_detailsFragment"
            app:destination="@id/detailsFragment" />
    </fragment>
    <dialog
        android:id="@+id/about"
        android:name="com.blogspot.abtallaldigital.ui.AboutFragment"
        android:label="about"
        tools:layout="@layout/about" />
    <fragment
        android:id="@+id/detailsFragment"
        android:name="com.blogspot.abtallaldigital.ui.DetailsFragment"
        android:label="Post details"
        tools:layout="@layout/fragment_details" >
        <argument
            android:name="postItem"
            app:argType="com.blogspot.abtallaldigital.pojo.Item" />
    </fragment>
    <fragment
        android:id="@+id/nav_favorites"
        android:name="com.blogspot.abtallaldigital.ui.FavoritesFragment"
        android:label="Favorites posts"
        tools:layout="@layout/fragment_favorites" >
        <action
            android:id="@+id/action_favoritesFragment_to_detailsFragment"
            app:destination="@id/detailsFragment" />
    </fragment>
</navigation>

MainActivity class

@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {

    @SuppressWarnings("unused")
    private AppBarConfiguration mAppBarConfiguration;
    public static Utils.DataStoreRepository DATA_STORE_REPOSITORY;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());


        setSupportActionBar(binding.appBarMain.toolbar);
        mAppBarConfiguration = new AppBarConfiguration.Builder(R.id.nav_home, R.id.nav_accessory,
                R.id.nav_arcade, R.id.nav_fashion,
                R.id.nav_food, R.id.nav_heath,
                R.id.nav_lifestyle, R.id.nav_sports, R.id.about)
                .setOpenableLayout(binding.drawerLayout)
                .build();


        NavHostFragment navHostFragment = (NavHostFragment)
                getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);

        assert navHostFragment != null;
        NavController navController = navHostFragment.getNavController();

        NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
        NavigationUI.setupWithNavController(binding.navView, navController);


    }


    @Override
    public boolean onSupportNavigateUp() {
        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
        return NavigationUI.navigateUp(navController, mAppBarConfiguration)
                || super.onSupportNavigateUp();
    }

}

Solution

  • Disclaimer:

    As the SharedPreference will be deprecated soon or later, there is an Update below using DataStore.


    Using SharedPreference

    onSaveInstanceState & onRestoreInstanceState can't be used to store/restore values after the app is closed/shut.

    Even if the app is not closed, you can't rely on them for storing large objects or storing objects for a long time.

    Instead of that you can use SharedPreference to store a value that maps to last open fragment before the app exists.

    Here I store some arbitrary value, as it's recommended not to store application IDs, as they can vary from app launch to another. So, you can store arbitrary values and map them to the generated IDs in the current app launch.

    I picked those values as array indices:

    // Array of fragments
    private Integer[] fragments = {
        R.id.nav_home, 
        R.id.nav_accessory,
        R.id.nav_arcade, 
        R.id.nav_fashion,
        R.id.nav_food, 
        R.id.nav_heath,
        R.id.nav_lifestyle, 
        R.id.nav_sports, 
        R.id.about
    };
    

    Then for every launch of the app; i.e. in onCreate() method, you can pick the current index from the SharedPreference, and call graph.setStartDestination():

    // Getting the last fragment:
    SharedPreferences mSharedPrefs = getSharedPreferences("SHARED_PREFS", MODE_PRIVATE);
    int fragIndex = mSharedPrefs.getInt(LAST_FRAGMENT, -1); // The last fragment index 
    // Check if it's a valid index
    if (fragIndex >= 0 && fragIndex < fragments.length) {
        // Navigate to this fragment
        int currentFragment = fragments[fragIndex];
        graph.setStartDestination(currentFragment);
    
        // Change the current navGraph
        navController.setGraph(graph);
    } 
    

    And you can register new values to the sharedPreference once the destination is changed using OnDestinationChangedListener of the navController:

    // Listener to the change in fragments, so that we can updated the shared preference
    navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
        int fragmentIndex = Arrays.asList(fragments).indexOf(destination.getId());
        SharedPreferences.Editor editor = mSharedPrefs.edit();
        editor.putInt(LAST_FRAGMENT, fragmentIndex).apply();
    });
    

    Integrating this into your code with:

    @AndroidEntryPoint
    public class MainActivity extends AppCompatActivity {
    
        private static final String TAG = "MainActivity";
        @SuppressWarnings("unused")
        private AppBarConfiguration mAppBarConfiguration;
        private NavHostFragment navHostFragment;
        private  NavController navController;
        NavGraph navGraph;
    
        
        // Array of fragments
        private Integer[] fragments = {
            R.id.nav_home, 
            R.id.nav_accessory,
            R.id.nav_arcade, 
            R.id.nav_fashion,
            R.id.nav_food, 
            R.id.nav_heath,
            R.id.nav_lifestyle, 
            R.id.nav_sports, 
            R.id.about
        };
        
        // Key for saving the last fragment in the Shared Preferences
        private static final String LAST_FRAGMENT = "LAST_FRAGMENT";
        
        
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
            setContentView(binding.getRoot());
    
    
            setSupportActionBar(binding.appBarMain.toolbar);
            mAppBarConfiguration = new AppBarConfiguration.Builder(R.id.nav_home, R.id.nav_accessory,
                    R.id.nav_arcade, R.id.nav_fashion,
                    R.id.nav_food, R.id.nav_heath,
                    R.id.nav_lifestyle, R.id.nav_sports, R.id.about)
                    .setOpenableLayout(binding.drawerLayout)
                    .build();
    
    
            navHostFragment = (NavHostFragment)
                    getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
    
            if(navHostFragment !=null) {
              navController = navHostFragment.getNavController();
            }
    
            NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
            NavigationUI.setupWithNavController(binding.navView, navController);
    
            navGraph = navController.getNavInflater().inflate(R.navigation.mobile_navigation);
    
            
            // Getting the last fragment:
            SharedPreferences mSharedPrefs = getSharedPreferences("SHARED_PREFS", MODE_PRIVATE);
            int fragIndex = mSharedPrefs.getInt(LAST_FRAGMENT, -1); // The last fragment index 
            // Check if it's a valid index
            if (fragIndex >= 0 && fragIndex < fragments.length) {
                // Navigate to this fragment
                int currentFragment = fragments[fragIndex];
                graph.setStartDestination(currentFragment);
    
                // Change the current navGraph
                navController.setGraph(graph);
            } 
            
    
            
            // Listener to the change in fragments, so that we can updated the shared preference
            navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
                int fragmentIndex = Arrays.asList(fragments).indexOf(destination.getId());
                SharedPreferences.Editor editor = mSharedPrefs.edit();
                editor.putInt(LAST_FRAGMENT, fragmentIndex).apply();
            });
            
    
        }
    
    
        @Override
        public boolean onSupportNavigateUp() {
            return NavigationUI.navigateUp(navController, mAppBarConfiguration)
                    || super.onSupportNavigateUp();
        }
    
    }
    

    Using DataStore

    Since I migrated from sharedpreferences to dataStore in this project, I tried to do the same as your solution but it doesn't work, I'll post my try and you can look at it to see what's wrong, then you can edit your answer with dataStore soultion

    So, I am going to use the same approach but with the DataStore (same fragment array, store indices instead of fragment destination IDs).

    So, you need to add the array of fragment IDs so that it can be read in the DataStore process:

    // Array of fragment IDs
    private Integer[] fragments = {
        R.id.nav_home, 
        R.id.nav_accessory,
        R.id.nav_arcade, 
        R.id.nav_fashion,
        R.id.nav_food, 
        R.id.nav_heath,
        R.id.nav_lifestyle, 
        R.id.nav_sports, 
        R.id.about
    };
    

    Then in the Repository change the logic to use the indices instead of the fragment IDs:

    @Inject
    public DataStoreRepository(@ApplicationContext Context context) {
        dataStore =
                new RxPreferenceDataStoreBuilder(Objects.requireNonNull(context), /*name=*/ "settings").build();
            
        readCurrentDestination =
            dataStore.data().map(preferences -> {
    
                Integer fragIndex = preferences.get(CURRENT_DESTINATION);
                if (fragIndex == null) fragIndex = 0;
    
                if (fragIndex >= 0 && fragIndex <= fragments.length) {
                    // Navigate to the fragIndex
                    return fragments[fragIndex];
                } else {
                    return R.id.nav_home;
                }
            }); 
        
    }   
    

    And in the ViewModel, you should not subscribe a permanent Observable to the Flowable because this will submit any change to the observed data permanently, but instead you can convert the Flowable to a Single so that you can just get a single (first) value of the fragment ID only once at the app launch, and no more observers are registered. Check Documentation for more details.

    Applying that in your ViewModel:

    @Inject
    public PostViewModel(Repository repository, Utils.DataStoreRepository dataStoreRepository) {
        this.repository = repository;
        getAllItemsFromDataBase = repository.localDataSource.getAllItems();
        this.dataStoreRepository = dataStoreRepository;
    
        dataStoreRepository.readCurrentDestination.firstOrError().subscribeWith(new DisposableSingleObserver<Integer>() {
    
            @Override
            public void onSuccess(@NotNull Integer destination) {
            
                // Must be run at UI/Main Thread
                runOnUiThread(() -> {
                    currentDestination.setValue(destination);
                });
                
            }
    
            @Override
            public void onError(@NotNull Throwable error) {
                error.printStackTrace();
            }
        }).dispose();
    
    }
    

    Then as you observe the currentDestination MutableLiveData in the activity: change the current destination there (You already did that well):

    postViewModel.currentDestination.observe(this,currentDestination -> {
        Log.d(TAG, "currentDestination: " + currentDestination);
        Toast.makeText(this,"currentDestination" + currentDestination,Toast.LENGTH_SHORT).show();
        navGraph.setStartDestination(currentDestination);
        navController.setGraph(navGraph);
    });
    

    Saving the current fragment to the DataStore whenever the destination changes:

    In the ViewModel:

    public void saveCurrentDestination(int value){
        int fragmentIndex = Arrays.asList(fragments).indexOf(value);
        CURRENT_DESTINATION = PreferencesKeys.intKey(keyName);
        dataStore.updateDataAsync(prefsIn -> {
            MutablePreferences mutablePreferences = prefsIn.toMutablePreferences();
            mutablePreferences.set(CURRENT_DESTINATION, fragmentIndex);
            return Single.just(mutablePreferences);
        }).subscribe();
    
    }