Android Navigation 如何动态的更换StartDestination &&保存Fragment状态
Navigation(一)基础入门
google 官网 : Navigation 导航 路由
讨论了两年的 Navigation 保存 Fragment 状态问题居然被关闭了
Navigation
是一种导航的概念,即把Activity
和fragment
当成一个个的目的地Destination
,各目的地形成一张导航图NavGraph
,由导航控制器NavController
来统一调度跳转
单个Activity嵌套多个Fragment的UI架构方式,已被大多数Android工程师所接受和采用。但是,对Fragment的管理一直是一个比较麻烦的事情,工程师需要通过FragmentManager和FragmentTransaction来管理Fragment之间的切换。这其中还包括了对应用程序的App bar的管理,Fragment间的切换动画,Fragment间的参数传递,总之,使用起来不是特别友好。
为此,Android Jetpack提供的一个名为Navigation的UI架构组件。旨在方便我们管理Fragment页面。它具体有以下优势:
注意:在Android Studio3.2及以上版本才能支持Navigation特性。
本文所说的“页面”包括了Fragment和Activity,但主要是Fragment,因为Navigation组件的主要目地就是方便我们在一个Activity中对多个Fragment进行管理。
首先,我们需要先对Navigation有一个大致的了解。
这是一种新型的XML资源文件,里面包含了应用程序所有的页面及页面之间的关系
这是一个特殊的布局文件,Navigation Graph中的页面通过该Fragment展示
这是一个Java/Kotlin对象,用于在代码中完成Navigation Graph中具体的页面切换
当你想要切换页面的时候,使用NavController
对象,告诉它你想要去Navigation Graph
中的哪个页面,NavController
会将相关的页面展示在NavHostFragment
中。
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.xq.mybottomnavigation"
minSdkVersion 29
targetSdkVersion 30
versionCode 1
versionName "1.0"
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.navigation:navigation-fragment:2.0.0'
implementation 'androidx.navigation:navigation-ui:2.0.0'
}
import android.os.Bundle;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import com.xq.mybottomnavigation.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
BottomNavigationView navView = findViewById(R.id.nav_view);
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
//获取App bar配置:AppBarConfiguration
AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications)
.build();
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_activity_main);
//将NavController和AppBarConfiguration进行绑定
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
//将需要交互的App barUI与NavController和AppBarConfiguration进行绑定
NavigationUI.setupWithNavController(binding.navView, navController);
}
}
navigation pop 和 push的时候 对Fragment的 操作是 replace,所以会导致生命周期重新走一遍
其实Navigation使用很简单,navigation和activity(确切的说是Fragment)绑定之后,使用两个方法就行,一个是navigate
,就是跳转,一个是navigateUp
,就是返回。
如果想要跳转到新页面时,在Fragment中使用:
NavHostFragment.findNavController(this).navigate(destinationID, bundle);
NavHostFragment.findNavController(this).navigateUp();
布局:
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="?attr/actionBarSize">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/bottom_nav_menu" />
<fragment
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
androidx.constraintlayout.widget.ConstraintLayout>
android:name="androidx.navigation.fragment.NavHostFragment"
这句话是在告诉系统,这是一个特殊的Fragment 。
app:defaultNavHost="true"
将defaultNavHost属性设置为true,则该Fragment会自动处理系统返回键,即,当用户按下手机的返回按钮时,系统能自动将当前的Fragment推出。
app:navGraph="@navigation/nav_graph"
设置该Fragment对应的导航图 。
<?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_home">
<fragment
android:id="@+id/navigation_home"
android:name="com.xq.mybottomnavigation.ui.home.HomeFragment"
android:label="@string/title_home"
tools:layout="@layout/fragment_home" />
<fragment
android:id="@+id/navigation_dashboard"
android:name="com.xq.mybottomnavigation.ui.dashboard.DashboardFragment"
android:label="@string/title_dashboard"
tools:layout="@layout/fragment_dashboard" />
<fragment
android:id="@+id/navigation_notifications"
android:name="com.xq.mybottomnavigation.ui.notifications.NotificationsFragment"
android:label="@string/title_notifications"
tools:layout="@layout/fragment_notifications" />
</navigation>
三个fragment类似
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import com.xq.mybottomnavigation.R;
import com.xq.mybottomnavigation.databinding.FragmentHomeBinding;
public class HomeFragment extends Fragment {
private HomeViewModel homeViewModel;
private FragmentHomeBinding binding;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
homeViewModel =
new ViewModelProvider(this).get(HomeViewModel.class);
binding = FragmentHomeBinding.inflate(inflater, container, false);
View root = binding.getRoot();
final TextView textView = binding.textHome;
homeViewModel.getText().observe(getViewLifecycleOwner(), new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
textView.setText(s);
}
});
return root;
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
<androidx.constraintlayout.widget.ConstraintLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.home.HomeFragment">
<TextView
android:id="@+id/text_home"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
androidx.constraintlayout.widget.ConstraintLayout>
//方式一 、 通过NavController
NavController controller = Navigation
.findNavController(this, R.id.nav_host_fragment_activity_main);
binding.btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
controller.navigate(R.id.navigation_notifications);
}
});
//方式二 、 通过NavHostFragment
NavHostFragment fragment = (NavHostFragment) getSupportFragmentManager()
.findFragmentById(R.id.nav_host_fragment_activity_main);
binding.btn2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (fragment != null) {
NavHostFragment.findNavController(fragment)
.navigate(R.id.navigation_notifications);
}
}
});
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
NavHostFragment.findNavController(NotificationsFragment.this).navigateUp();
}
});
MainActivity : 发送数据
NavController controller = Navigation
.findNavController(this, R.id.nav_host_fragment_activity_main);
NavInflater navInflater = controller.getNavInflater();
NavGraph navGraph = navInflater.inflate(R.navigation.mobile_navigation);
navGraph.setStartDestination(R.id.navigation_notifications);//初始界面
Bundle args = new Bundle();
args.putBoolean("6no6", true);//传参
navController.setGraph(navGraph, args);
或者
//动态加载setGraph
FragmentManager manager = getSupportFragmentManager();
NavHostFragment hostFragment = (NavHostFragment) manager
.findFragmentById(R.id.nav_host_fragment_activity_main);
NavController controller = null;
if (hostFragment != null) {
controller = hostFragment.getNavController();
}
navController.setGraph(R.navigation.mobile_navigation);
NotificationsFragment : 接收数据
Bundle arguments = getArguments();
if (arguments != null) {
boolean aBoolean = arguments.getBoolean("6no6");
textView.setTextColor(aBoolean ? Color.RED : Color.BLUE);
}
navController.addOnDestinationChangedListener(new NavController.OnDestinationChangedListener() {
@Override
public void onDestinationChanged(@NonNull @NotNull NavController controller,
@NonNull @NotNull NavDestination destination,
@Nullable Bundle arguments) {
CharSequence label = destination.getLabel();
Log.e(TAG, "onDestinationChanged: ===="+label);
}
});
在进行跳转时 直接使用了replace,所以导致当前页面会调用 onDestroyView,即fragment变为 inactive,当进行pop操作时,fragment重新进入 active状态时,会重新调用 onViewCreated 等方法,导致页面重新绘制,
其实在这种情况下,我们可以直接用ViewModel
和LiveData
对数据进行保存,但是这次想尝试一下新的解决办法。
在知道原因后就好办了,直接继承FragmentNavigator
把方法重写
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
// ft.replace(mContainerId, frag);
// change to
if(mFragmentManager.getFragments().size()>0){
ft.hide(mFragmentManager.getFragments().get(mFragmentManager.getFragments().size()-1));
ft.add(mContainerId, frag);
}else {
ft.replace(mContainerId, frag);
}
}
KeepStateFragmentNavigator 使用如下:
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
Fragment navHostFragment = getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
navController.getNavigatorProvider()
.addNavigator(new KeepStateFragmentNavigator(this,
navHostFragment.getChildFragmentManager(),
R.id.nav_host_fragment));
NavHostFragment.findNavController(this).navigate(destinationID, bundle);