美文网首页Android Jetpack中遇到的问题
Android jetpack关于ButtomNavigatio

Android jetpack关于ButtomNavigatio

作者: LATLAJ | 来源:发表于2020-02-27 17:39 被阅读0次

fragment的切换问题一直都比较麻烦,好在官方最新的jetpack提供了简单的解决方案,但也是不能拿来直接用,在此记录下一些解决方法。

1.创建Android studio默认ButtomNavigationView

MainActivity的布局很简单,一个fragment一个BottomNavigationView,注意到ConstraintLayout有个paddingTop="?attr/actionBarSize"这就比较奇怪,貌似如果父布局有toolbar时才有用。fragment的类是NavHostFragment,这就是jetpack的组件了。注意app:menu指定了NavigationView的menu,fragment的app:navGraph指定了一个navigation文件。

<?xml version="1.0" encoding="utf-8"?>
<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"
        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>

我们打开res/navigation/mobile_navigation,有三个fragment标签,其父标签navigation通过app:startDestination指定了起始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.latlaj.buttomnavigationtest.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" />

    <fragment
        android:id="@+id/navigation_dashboard"
        android:name="com.latlaj.buttomnavigationtest.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard" />

    <fragment
        android:id="@+id/navigation_notifications"
        android:name="com.latlaj.buttomnavigationtest.ui.notifications.NotificationsFragment"
        android:label="@string/title_notifications"
        tools:layout="@layout/fragment_notifications" />
</navigation>

打开src/menu/bottom_nav_menu。

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

    <item
        android:id="@+id/navigation_home"
        android:icon="@drawable/ic_home_black_24dp"
        android:title="@string/title_home" />

    <item
        android:id="@+id/navigation_dashboard"
        android:icon="@drawable/ic_dashboard_black_24dp"
        android:title="@string/title_dashboard" />

    <item
        android:id="@+id/navigation_notifications"
        android:icon="@drawable/ic_notifications_black_24dp"
        android:title="@string/title_notifications" />

</menu>

每个item有和navigation中fragment同样的id,通过下面MainActivity中onCreate用NavigationUI的方法setupWithNavController的绑定实现跳转。

BottomNavigationView navView = findViewById(R.id.nav_view);
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupWithNavController(navView, navController);

2.用ViewModel保存View数据

整个fragment代码非常简洁,简直是新手的福音,但我后来发现一个崩溃的事,每次切换fragment,之前的fragment已经完全Destroy了,后退时完全重走了一遍Fragment的onAttach()->onCreate()->onCreateView()...的过程,运存倒是省了,可是需要联网显示的或滚动界面的fragment无法恢复状态是不可接受的。我当然想到了项目默认创建的ViewModel,可是依赖fragment的ViewModel在fragment destroy之后生命周期也结束了。 Bottom Navigation Activity项目每个fragment都有ViewModel.png

最后用Activity的ViewModel解决问题。新建MainViewModel.java:

package com.latlaj.buttomnavigationtest;
import android.view.View;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
public class MainViewModel extends ViewModel {
    private MutableLiveData<View> mDashboardView;
    private MutableLiveData<View> mHomeView;
    private MutableLiveData<View> mNotificationsView;
    public MainViewModel() {
        mDashboardView = new MutableLiveData<>();
        mHomeView = new MutableLiveData<>();
        mNotificationsView = new MutableLiveData<>();
    }
    public void setDashboardView(View v) {
        mDashboardView.setValue(v);
    }
    public void setHomeView(View v) {
        mHomeView.setValue(v);
    }
    public void setNotificationsView(View v) {
        mNotificationsView.setValue(v);
    }
    public LiveData<View> getDashboardView() {
        return mDashboardView;
    }
    public LiveData<View> getHomeView() {
        return mHomeView;
    }
    public LiveData<View> getNotificationsView() {
        return mNotificationsView;
    }
}

使用方法,其实拿到的是同一个mainViewModel实例:

//在MainActivity中使用
MainViewModel mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
//在HomeFragment中使用,在onViewCreated()中或之后调用
MainViewModel mainViewModel = new ViewModelProvider(getActivity()).get(MainViewModel.class);

以HomeFragment为例,我们不使用ViewModel的观察者方法,直接在onDestroyView调用时保存View。

@Override
public void onDestroyView() {
    super.onDestroyView();
    mainViewModel.setView(root);
}
private View root;
private MainViewModel mainViewModel;
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
                         ViewGroup container, Bundle savedInstanceState) {
    mainViewModel=new ViewModelProvider(getActivity())
            .get(MainViewModel.class);
    root=mainViewModel.getHomeView().getValue();
    if(root==null){
        //正常初始化View
        root = inflater.inflate(R.layout.fragment_home, container, false);
        //...
    }
    //...
    return root;
}

然而这有一个问题,root在这种情况下可能会指定多个parent导致报错,加入去除parent的代码:

@Override
public void onDestroyView() {
    super.onDestroyView();
    //其实以下代码不一定写在onDestroyView(),可以获取指定后root的地方都可以
    ViewGroup parent = (ViewGroup) root.getParent();
    if(parent!=null){
        parent.removeView(root);
    }
    mainViewModel.setView(root);
}

3.取消烦人的fragment重建

到这里为止还有一个非常不爽的地方,就是我们单击已经选中的BottomNavigationView的底部Tag,居然会再次新建当前fragment,同时把旧fragment的视图设为不可见后销毁,这会导致我们重用视图的fragment闪烁、空白,于是我们要取消当前Tag的单击动作,观察NavigationUI的setupWithNavController方法源码: NavigationUI.setupWithNavController.png

发现只给navView设置了一个监听,于是我们可以重新设置监听:

BottomNavigationView navView = findViewById(R.id.nav_view);
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupWithNavController(navView, navController);
navView.setOnNavigationItemSelectedListener(
        new BottomNavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                if (item.isChecked()){
                    //...其他动作
                    return true;//单击事件被消耗
                }
                return onNavDestinationSelected(item, navController);
            }
        });

这样的结果是单击当前Tag不再有动作。
希望大家能提供更好的办法。

相关文章

网友评论

    本文标题:Android jetpack关于ButtomNavigatio

    本文链接:https://www.haomeiwen.com/subject/oapypxtx.html