1. 程式人生 > >Android Lifecycle--Navigation Architecture Component 使用詳解

Android Lifecycle--Navigation Architecture Component 使用詳解

二、準備工作

Navigation 是 Android Studio 3.2 才有的功能,所以要先下載 Android Studio 3.2, 目前 Android Studio 3.2 是預覽版,正式版目前是 3.1.3,
[Androi Studio 3.2 下載頁] [Androi Studio 3.2 下載連結]
執行結果截圖
執行結果截圖

三、Navigation 的用法

(一)基本用法

下載完 Android Studio 3.2 後開啟程式新建個專案,開啟 app 下的 build.gradle 匯入 Navigation

dependencies {
    implementation "android.arch.navigation:navigation-fragment:1.0.0-alpha02"
implementation "android.arch.navigation:navigation-ui:1.0.0-alpha02" }

新建個Fragment

public class FirstFragment extends Fragment {
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_simple_first, container, false
); } }

佈局

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
android:layout_height="match_parent" tools:context=".simple.FirstFragment">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" android:textSize="30sp" android:text="我是第一個 Fragment" /> </android.support.constraint.ConstraintLayout>

在 res 目錄右鍵選擇 New -> Android Resource File
執行結果截圖
新建個 Navigation 資原始檔
執行結果截圖
新建完成就會在 res 目錄下生成 navigation 目錄和檔案,就是下面那樣的 根元素是 navigation
執行結果截圖
執行結果截圖
接下來就把剛剛寫的 Fragment 寫進去,打上左尖括號 < Android Studio 就會提示
執行結果截圖
這裡選擇 fragment 標籤,選擇了 fragment 後再打個空格又有提示
執行結果截圖
這裡的 id 就像寫佈局的 id 那樣需要給個 id 才能找到它,name 就是說明是哪個 Fragment 類名的,像下面那樣

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android">
    <fragment
        android:id="@+id/nav_simple_first_frag"
        android:name="com.ce.navigationtest.simple.FirstFragment"
        android:label="first frag" >
    </fragment>
</navigation>

這時可以點選下面的 Design 看一下
執行結果截圖
執行結果截圖
這什麼呀,Preview Unavailable? 預覽不可用?(黑人問號臉),其實這裡是少寫了個 layout 的屬性,Android Studio 也沒提示,可能是預覽版的還不夠完善的原因,layout 屬性是要用到 tools 的名稱空間的,加上 layout 後如下,

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <fragment
        android:id="@+id/nav_simple_first_frag"
        android:name="com.ce.navigationtest.simple.FirstFragment"
        android:label="first frag"
        tools:layout="@layout/fragment_simple_first">
    </fragment>
</navigation>

再去 Design 看一下,這回就有了
執行結果截圖
這裡先告一段落了(順便挖了個坑),現在去寫 Activity 裡的佈局,Activity 的佈局該怎麼寫?這裡要用到 NavHost 來託管 Navigation,NavHost 是個介面,預設是用 NavHostFragment 來託管,NavHostFragment 是實現了 NavHost 介面的,進去 NavHostFragment 看一下

/**
 * NavHostFragment provides an area within your layout for self-contained navigation to occur.
 *
 * <p>NavHostFragment is intended to be used as the content area within a layout resource
 * defining your app's chrome around it, e.g.:</p>
 *
 * <pre class="prettyprint">
 *     <android.support.v4.widget.DrawerLayout
 *             xmlns:android="http://schemas.android.com/apk/res/android"
 *             xmlns:app="http://schemas.android.com/apk/res-auto"
 *             android:layout_width="match_parent"
 *             android:layout_height="match_parent">
 *         <fragment
 *                 android:layout_width="match_parent"
 *                 android:layout_height="match_parent"
 *                 android:id="@+id/my_nav_host_fragment"
 *                 android:name="androidx.navigation.fragment.NavHostFragment"
 *                 app:navGraph="@xml/nav_sample"
 *                 app:defaultNavHost="true" />
 *         <android.support.design.widget.NavigationView
 *                 android:layout_width="wrap_content"
 *                 android:layout_height="match_parent"
 *                 android:layout_gravity="start"/>
 *     </android.support.v4.widget.DrawerLayout>
 * </pre>
 *
 * <p>Each NavHostFragment has a {@link NavController} that defines valid navigation within
 * the navigation host. This includes the {@link NavGraph navigation graph} as well as navigation
 * state such as current location and back stack that will be saved and restored along with the
 * NavHostFragment itself.</p>
 *
 * <p>NavHostFragments register their navigation controller at the root of their view subtree
 * such that any descendant can obtain the controller instance through the {@link Navigation}
 * helper class's methods such as {@link Navigation#findNavController(View)}. View event listener
 * implementations such as {@link android.view.View.OnClickListener} within navigation destination
 * fragments can use these helpers to navigate based on user interaction without creating a tight
 * coupling to the navigation host.</p>
 */
public class NavHostFragment extends Fragment implements NavHost {
  ......
}

哇,註釋中怎麼寫都給我們準備好了,厲害厲害,我們拿來用就好了,我們先簡單點

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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=".simple.SimpleActivity">
    <fragment
        android:id="@+id/frag_nav_simple"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:navGraph="@navigation/nav_simple"
        app:defaultNavHost="true" />
</android.support.constraint.ConstraintLayout>

navGraph 屬性就是寫剛才我們寫的 nagation 檔案,defaultNavHost 這個是和返回鍵相關的,和這一塊相關的[官方文件]
到這裡可以去執行一下了,啥情況,咋崩潰了,剛才挖的什麼坑,來看一下日誌
執行結果截圖
no start destination defined via app:startDestination for the root navigation 黑人問號臉,有沒有注意過 navigation 檔案的 navigation 標籤有個警告,滑鼠移上去也有提示 No start destination specified
執行結果截圖
其實這裡是要在 navigation 檔案裡指定是從哪裡開始的,沒有指定就會報錯,因為不知道哪個是出發點,就像地圖得知道起始位置和目的地才可以導航,修改一下 navigation 檔案的內容,根 navigation 新增上 startDestination 屬性,

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:startDestination="@id/nav_simple_first_frag">
    <fragment
        android:id="@+id/nav_simple_first_frag"
        android:name="com.ce.navigationtest.simple.FirstFragment"
        android:label="first frag"
        tools:layout="@layout/fragment_simple_first">
    </fragment>
</navigation>

再執行一下吧,這回不坑了,
執行結果截圖

(二)介面間跳轉

一個 Fragment 怎麼過癮,再來個 Fragment

public class SecondFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_simple_second, container, false);
    }
}

佈局

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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=".simple.SecondFragment">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我是第二個 Fragment"
        android:textSize="30sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

把第一個 Fragment 的佈局也改一下,

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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=".simple.FirstFragment">
    <android.support.v7.widget.AppCompatButton
        android:id="@+id/btn_to_second_fragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="去第二個Fragment"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

把第二個 Fragment 也新增到 Navigation 檔案裡,和第一個 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"
    app:startDestination="@id/nav_simple_first_frag">
    ......
    <fragment
        android:id="@+id/nav_simple_second_frag"
        android:name="com.ce.navigationtest.simple.SecondFragment"
        android:label="second frag"
        tools:layout="@layout/fragment_simple_second">
    </fragment>
</navigation>

那第一個 Fragment 怎麼和 第二個 Fragment 關聯起來?很簡單,有 action
執行結果截圖
執行結果截圖
這裡可以看到 action 有很多屬性,這裡現在我們只需要 id 和 destination,id 就是這個 action 的 id, destination 是目的地,要跳轉到哪裡的,這裡寫上第二個 Fragment 的 id。

<?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"
    app:startDestination="@id/nav_simple_first_frag">
    <fragment
        android:id="@+id/nav_simple_first_frag"
        android:name="com.ce.navigationtest.simple.FirstFragment"
        android:label="first frag"
        tools:layout="@layout/fragment_simple_first">
        <action
            android:id="@+id/action_nav_first_frag_to_nav_second_frag"
            app:destination="@id/nav_simple_second_frag" />
    </fragment>
    <fragment
        android:id="@+id/nav_simple_second_frag"
        android:name="com.ce.navigationtest.simple.SecondFragment"
        android:label="second frag"
        tools:layout="@layout/fragment_simple_second">
    </fragment>
</navigation>

到這裡可以去 Design 看看,第一個 Fragment 有個箭頭指向第二個 Fragment
執行結果截圖
Navigation 這裡已經完事,這回沒坑,去第一個 Fragment 給 Button 新增上點選事件,要跳轉到第二個 Fragment 得有 NavController,就是用來控制跳轉的,那怎麼得到,有三種方法,Navigation 類有兩種,

/**
 * Find a {@link NavController} given a local {@link View}.
 *
 * <p>This method will locate the {@link NavController} associated with this view.
 * This is automatically populated for views that are managed by a {@link NavHost}
 * and is intended for use by various {@link android.view.View.OnClickListener listener}
 * interfaces.</p>
 *
 * @param view the view to search from
 * @return the locally scoped {@link NavController} to the given view
 * @throws IllegalStateException if the given view does not correspond with a
 * {@link NavHost} or is not within a NavHost.
 */
@NonNull
public static NavController findNavController(@NonNull View view) {
    NavController navController = findViewNavController(view);
    if (navController == null) {
        throw new IllegalStateException("View " + view + " does not have a NavController set");
    }
    return navController;
}
/**
 * Find a {@link NavController} given the id of a View and its containing
 * {@link Activity}. This is a convenience wrapper around {@link #findNavController(View)}.
 *
 * <p>This method will locate the {@link NavController} associated with this view.
 * This is automatically populated for the id of a {@link NavHost} and its children.</p>
 *
 * @param activity The Activity hosting the view
 * @param viewId The id of the view to search from
 * @return the {@link NavController} associated with the view referenced by id
 * @throws IllegalStateException if the given viewId does not correspond with a
 * {@link NavHost} or is not within a NavHost.
 */
@NonNull
public static NavController findNavController(@NonNull Activity activity, @IdRes int viewId) {
    View view = ActivityCompat.requireViewById(activity, viewId);
    NavController navController = findViewNavController(view);
    if (navController == null) {
        throw new IllegalStateException("Activity " + activity
                + " does not have a NavController set on " + viewId);
    }
    return navController;
}

還有一種是通過 NavHostFragment 類

/**
 * Find a {@link NavController} given a local {@link Fragment}.
 *
 * <p>This method will locate the {@link NavController} associated with this Fragment,
 * looking first for a {@link NavHostFragment} along the given Fragment's parent chain.
 * If a {@link NavController} is not found, this method will look for one along this
 * Fragment's {@link Fragment#getView() view hierarchy} as specified by
 * {@link Navigation#findNavController(View)}.</p>
 *
 * @param fragment the locally scoped Fragment for navigation
 * @return the locally scoped {@link NavController} for navigating from this {@link Fragment}
 * @throws IllegalStateException if the given Fragment does not correspond with a
 * {@link NavHost} or is not within a NavHost.
 */
@NonNull
public static NavController findNavController(@NonNull Fragment fragment) {
    Fragment findFragment = fragment;
    while (findFragment != null) {
        if (findFragment instanceof NavHostFragment) {
            return ((NavHostFragment) findFragment).getNavController();
        }
        Fragment primaryNavFragment = findFragment.requireFragmentManager()
                .getPrimaryNavigationFragment();
        if (primaryNavFragment instanceof NavHostFragment) {
            return ((NavHostFragment) primaryNavFragment).getNavController();
        }
        findFragment = findFragment.getParentFragment();
    }
    // Try looking for one associated with the view instead, if applicable
    View view = fragment.getView();
    if (view != null) {
        return Navigation.findNavController(view);
    }
    throw new IllegalStateException("Fragment " + fragment
            + " does not have a NavController set");
}

都是 public static 的方法,所以得到 NavController 之後呢,NavController 有 navigate 方法可以做跳轉的

/**
 * Navigate to a destination from the current navigation graph. This supports both navigating
 * via an {@link NavDestination#getAction(int) action} and directly navigating to a destination.
 *
 * @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
 *              navigate to
 */
public final void navigate(@IdRes int resId) {
    navigate(resId, null);
}

這裡的引數 resId ,從註釋中也知道是 action 的那個 id, 所以,趕緊給按鈕新增事件做跳轉啊,

public class FirstFragment extends Fragment {
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_simple_first, container, false);
    }
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        Button btnFirstToSecond = view.findViewById(R.id.btn_to_second_fragment);
        btnFirstToSecond.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //NavHostFragment.findNavController(FirstFragment.this).navigate(R.id.action_nav_first_frag_to_nav_second_frag);
                //Navigation.findNavController(getActivity(), R.id.btn_to_second_fragment).navigate(R.id.action_nav_first_frag_to_nav_second_frag);
                Navigation.findNavController(getView()).navigate(R.id.action_nav_first_frag_to_nav_second_frag);
            }
        });
    }
}

執行結果截圖

上面註釋掉的兩個也是可以跳轉的

(三)介面切換動畫

介面是不是感覺很生硬?一點就跳過去了,在 action 那可以看到有有幾個 anim,對的,可以新增動畫,添加個淡入淡出的動畫吧,

fade_in

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:duration="500"
        android:fromAlpha="0.0"
        android:toAlpha="1.0"/>
</set>

fade_out

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:duration="500"
        android:fromAlpha="1.0"
        android:toAlpha="0.0"/>
</set>
<?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"
    app:startDestination="@id/nav_simple_first_frag">
    <fragment
        android:id="@+id/nav_simple_first_frag"
        android:name="com.ce.navigationtest.simple.FirstFragment"
        android:label="first frag"
        tools:layout="@layout/fragment_simple_first">
        <action
            android:id="@+id/action_nav_first_frag_to_nav_second_frag"
            app:destination="@id/nav_simple_second_frag"
            app:enterAnim="@anim/fade_in"
            app:exitAnim="@anim/fade_out"
            app:popEnterAnim="@anim/fade_in"
            app:popExitAnim="@anim/fade_out"/>
    </fragment>
    ......
</navigation>

看看效果,是不是好些了
執行結果截圖

(四)資料傳遞

有時候可能要從第一個 Fragment 帶些資料去第二個 Fragment,那怎麼辦,也很簡單,navigate 有個倆引數的方法

/**
 * Navigate to a destination from the current navigation graph. This supports both navigating
 * via an {@link NavDestination#getAction(int) action} and directly navigating to a destination.
 *
 * @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
 *              navigate to
 * @param args arguments to pass to the destination
 */
public final void navigate(@IdRes int resId, @Nullable Bundle args) {
    navigate(resId, args, null);
}

第二個引數 Bundle 是經常用的了,跳轉後 Activity 可以用 getIntent() 獲取,Fragment 可以通過 getArguments() 獲取,試試吧

public class FirstFragment extends Fragment {
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_simple_first, container, false);
    }
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        Button btnFirstToSecond = view.findViewById(R.id.btn_to_second_fragment);
        btnFirstToSecond.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Bundle bundle = new Bundle();
                bundle.putString("KEY", "我是從 First 過來的");
                Navigation.findNavController(getView()).navigate(R.id.action_nav_first_frag_to_nav_second_frag, bundle);
            }
        });
    }
}
public class SecondFragment extends Fragment {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Bundle arguments = getArguments();
        String data = arguments.getString("KEY");
        Toast.makeText(getContext(), data, Toast.LENGTH_SHORT).show();
    }
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_simple_second, container, false);
    }
}

這裡是 Fragment ,跳轉後用 getArguments() 去獲取,
執行結果截圖

(五)型別安全的方式傳遞資料

Navigation 還提供了一種安全的資料傳遞,是怎樣的呢?先配置安全外掛,

在 Project 下的 build.gradle

buildscript {
    ......
    dependencies {
        ......
        classpath 'android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha02'
    }
}

在 app 下的 build.gradle 裡 apply, 同步一下 gradle

apply plugin: 'com.android.application'
apply plugin: 'androidx.navigation.safeargs'
android {
    ......
}

配置完就開始吧,在 fragment 元素裡打上 < 會出現 argument 就是我們想要的
執行結果截圖
argument 有三個屬性 name、defaultValue 和 type, name 就是名字到時會生成這個名字的 set 和 get 方法,defaultValue 是預設值,type 就是資料型別
執行結果截圖
資料型別這一塊我沒找到相關文件有什麼資料型別可以傳輸的,我試過八種基本資料型別和 String 型別,只有 boolean、integer、float 和 string 可以,各位有知道有相關文件或其他型別的還望告知
執行結果截圖
執行結果截圖
那就試試那四種可以的型別

<fragment
    android:id="@+id/nav_simple_second_frag"
    android:name="com.ce.navigationtest.simple.SecondFragment"
    android:label="second frag"
    tools:layout="@layout/fragment_simple_second">
    <action
        android:id="@+id/action_nav_second_frag_to_nav_third_frag"
        app:destination="@id/nav_simple_third_frag" />
    <argument android:name="booleanData" app:type="boolean" />
    <argument android:name="intData" app:type="integer" />
    <argument android:name="floatData" app:type="float" />
    <argument android:name="stringData" app:type="string" />
</fragment>

完了就 rebuild 一下,讓安全外掛生成相關的類,末尾有 Directions 是 Destination 裡有 action 的,末尾帶有 Args 是 Destination 裡有 argument 的
執行結果截圖
怎麼用呢?使用也簡單,生成的 argument 的類使用 Builder 模式,這裡的資料是從第一個 Fragment 傳資料給第二個 Fragment

第一個 Fragment 的按鈕點選事件處理,

btnFirstToSecond.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        SecondFragmentArgs fragmentArgs = new SecondFragmentArgs
                .Builder(true,
                1,
                1.1f,
                "我是通過 argument 過來的")
                .build();
        Navigation.findNavController(getView())
                .navigate(R.id.action_nav_first_frag_to_nav_second_frag, fragmentArgs.toBundle());
    }
});

用 Builder 得到 Argument 類的物件後,Argument 類有個 toBundle() 方法會生成 Bundle 物件並把資料填充進這個 Bundle 物件裡

第二個 Fragment 接收,

public class SecondFragment extends Fragment {
    private static final String TAG = "SecondFragment";
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SecondFragmentArgs fragmentArgs = SecondFragmentArgs.fromBundle(getArguments());
        Log.v(TAG, "boolean data = " + fragmentArgs.getBooleanData());
        Log.v(TAG, "int data = " + fragmentArgs.getIntData());
        Log.v(TAG, "float data = " + fragmentArgs.getFloatData());
        Log.v(TAG, "string data = " + fragmentArgs.getStringData());
    }
    ......
}

在 onCreate 裡,因為 argument 類有個 fromBundle(Bundle bundle),把 getArguments() 傳進去 fromBundle() 方法返回 argument 類的物件,然後使用 get 去獲取資料,
執行結果截圖

(六)返回

還記得前面說過 defaultNavHost 這個屬性麼?和返回鍵有關的,如果把這個屬性改為 false,從第一個 Fragment 跳到第二個 Fragment 再按返回鍵就會直接退出程式
執行結果截圖
第二個 Fragment 可以不用按返回鍵返回第一個 Fragment, 通過 NavController 去控制,修改下第二個 Fragment 的佈局,

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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=".simple.SecondFragment">
    <android.support.v7.widget.AppCompatButton
        android:id="@+id/btn_back_first_fragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="返回第一個Fragment"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

NavController 有 navigateUp() 和 popBackStack() 都可以返回上一級,有什麼區別? popBackStack() 如果當前的返回棧是空的就會報錯,因為棧是空的了,navigateUp() 則不會,還是停留在當前介面,好了,該給第二個 Fragment 新增事件返回了,

public class SecondFragment extends Fragment {
    ......
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        Button btnBackFristFrag = view.findViewById(R.id.btn_back_first_fragment);
        btnBackFristFrag.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Navigation.findNavController(getView()).navigateUp();
            }
        });
    }
}

執行結果截圖

試試給第一個 Fragment 的按鈕來個 popBackStack() 會怎樣?

public class FirstFragment extends Fragment {
    ......
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        Button btnFirstToSecond = view.findViewById(R.id.btn_to_second_fragment);
        btnFirstToSecond.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Navigation.findNavController(getView()).popBackStack();
            }
        });
    }
}

執行結果截圖

崩潰了,按第一下時沒事,第二次按下就崩潰了,來看看 log, NavController back stack is empty
執行結果截圖
看看 popBackStack() 原始碼,第一句就是判斷返回棧是不是空的

/**
 * Attempts to pop the controller's back stack. Analogous to when the user presses
 * the system {@link android.view.KeyEvent#KEYCODE_BACK Back} button when the associated
 * navigation host has focus.
 *
 * @return true if the stack was popped, false otherwise
 */
public boolean popBackStack() {
    if (mBackStack.isEmpty()) {
        throw new IllegalArgumentException("NavController back stack is empty");
    }
    boolean popped = false;
    while (!mBackStack.isEmpty()) {
        popped = mBackStack.removeLast().getNavigator().popBackStack();
        if (popped) {
            break;
        }
    }
    return popped;
}

那換 navigateUp() 試試,

public class FirstFragment extends Fragment {
    ......
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        Button btnFirstToSecond = view.findViewById(R.id.btn_to_second_fragment);
        btnFirstToSecond.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Navigation.findNavController(getView()).navigateUp();
            }
        });
    }
}

執行結果截圖

navigateUp() 點了很多下也沒