如何打开关闭应用程序后打开的最后一个片段并使用导航抽屉和导航组件重新打开它
Posted
技术标签:
【中文标题】如何打开关闭应用程序后打开的最后一个片段并使用导航抽屉和导航组件重新打开它【英文标题】:How to open the last fragment opened after closed app and reopen it using Navigation drawer and Navigation Component 【发布时间】:2021-11-29 07:46:47 【问题描述】:更新
由于onSaveInstanceState
& onRestoreInstanceState
在关闭应用后不能用于存储/恢复值,我尝试使用dataStore解决它,但它不起作用,这是我的尝试
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();
在 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);
最后是 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();
问题详解
在这个应用程序中,我在导航抽屉中有 9 个菜单项和片段,我想将最后打开的片段保存在 savedInstanceState
或 datastore
中,并在用户关闭应用程序并重新打开后再次显示最后打开的片段,但我不知道我会使用哪种方法
Navigation.findNavController(activity,nav_graph).navigate();
或
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_
android:layout_
android:fitsSystemWindows="true"
tools:openDrawer="start">
<include
android:id="@+id/app_bar_main"
layout="@layout/app_bar_main"
android:layout_
android:layout_ />
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_
android:layout_
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 类
@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();
【问题讨论】:
你read the documentation了吗?它以这种情况为例。 @ianhanniballake 谢谢我现在已经阅读了,我会继续寻找是否有其他方法可以做到这一点,而不是切换navGraph.startDestination
【参考方案1】:
免责声明:
由于SharedPreference
迟早会被弃用,下面有一个使用DataStore
的更新。
使用SharedPreference
在应用关闭/关闭后,onSaveInstanceState
& onRestoreInstanceState
不能用于存储/恢复值。
即使应用没有关闭,你也不能依赖它们来存储大型对象或长时间存储对象。
您可以使用SharedPreference
来存储一个值,该值映射到应用程序存在之前的最后一个打开片段。
这里我存储了一些任意值,因为建议不要存储应用程序 ID,因为它们可能因应用程序启动而异。因此,您可以存储任意值并将它们映射到当前应用启动时生成的 ID。
我选择这些值作为数组索引:
// 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
;
然后每次启动应用程序;即在onCreate()
方法中,您可以从SharedPreference
中选择当前索引,然后调用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);
您可以使用navController
的OnDestinationChangedListener
更改目标后向sharedPreference 注册新值:
// 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();
);
将其集成到您的代码中:
@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();
使用数据存储
自从我在这个项目中从 sharedpreferences 迁移到 dataStore 后,我尝试了与您的解决方案相同的操作,但它不起作用,我将发布我的尝试,您可以查看它以查看问题所在,然后您可以使用 dataStore soultion 编辑您的答案
所以,我将使用相同的方法,但使用 DataStore(相同的片段数组,存储索引而不是片段目标 ID)。
因此,您需要添加片段 ID 数组,以便在 DataStore 进程中读取它:
// 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
;
然后在存储库中更改逻辑以使用索引而不是片段 ID:
@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;
);
在ViewModel
中,您不应该为Flowable
订阅永久的 Observable,因为这将永久提交对观察到的数据的任何更改,但是您可以将Flowable
转换为Single
,以便您只能在应用程序启动时获得片段 ID 的单个(第一个)值,并且不再注册观察者。 Check Documentation 了解更多详情。
在你的 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();
然后当您观察活动中的currentDestination
MutableLiveData 时:更改那里的当前目的地(您已经做得很好):
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);
);
每当目标更改时,将当前片段保存到DataStore
:
在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();
【讨论】:
自从我在这个项目中从 sharedpreferences 迁移到 dataStore 后,我尝试与您的解决方案相同,但它不起作用,我将发布我的尝试,您可以查看它以查看是什么错了,那么您可以使用 dataStore soultion 编辑您的答案,我将不胜感激 @DrMido 太好了.. 我没有注意到您正在使用 DataStore;我刚刚更新了答案。希望它符合您的需要 我确实像你一样乱扔垃圾,但不幸的是它不起作用,当应用程序启动时,我收到一条日志和 toast 消息,其编号为 currentDestinationcurrentDestination: 2131296601
这是家庭 id ,它永远不会改变,它看起来 navController.addOnDestinationChangedListener
没有被调用,我也尝试了 navView.setNavigationItemSelectedListener
并获取项目 id 并将其存储到 currentDestination mutablelivedata 和同样的问题,缺少一些东西
@DrMido 我看到我错过了更新当前目标部分;请看答案UPDATE 2
部分
很高兴听到这个消息! @DrMido 对于第一点。片段 ID 由应用程序生成;并且建议不要在不同的应用程序启动时使用它们,因为它们可能因应用程序启动而异。因此,您可以存储一个不会在下次应用启动时映射到片段 ID 的值,因此它将转到默认片段(主页)。【参考方案2】:
如何在导航图中存储当前位置并不重要(我们实际上是在讨论存储单个 long
值以及最终的一些参数值)。
前提条件本身应该已经解释了它是如何工作的:
能够使用globalNavAction
导航到每个目的地。
能够将存储的目标 ID 解析为 global NavAction
。
不要忘记导航参数(例如itemId
)。
与此类似,可能会丢失回栈条目,但可以直接导航。如果这还不够,请存储 NavController
返回堆栈条目,然后播放它们(不建议)。
【讨论】:
为什么要保存动作本身以及为什么要存储动作本身,这可以很简单,只需在每次应用启动时使用navGraph.startDestination
,导航项 id destination.getId()
的 int 不长以上是关于如何打开关闭应用程序后打开的最后一个片段并使用导航抽屉和导航组件重新打开它的主要内容,如果未能解决你的问题,请参考以下文章