Android Jetpack架構組件(五)之Navigation

1、 Navigation簡介

1.1 Navigation誕生背景

採用單個Activity嵌套多個Fragment的UI架構模式,已經被大多數的Android工程師所接受。可是,對於Fragment的管理一直是一件比較麻煩的事情,工程師須要經過FragmentManager和FragmentTransaction來管理Fragment之間的切換。java

在Android中,頁面的切換和管理包括應用程序Appbar的管理、Fragment的動畫切換以及Fragment之間的參數傳遞等內容。而且,純代碼的方式使用起來不是特別友好,而且Appbar在管理和使用的過程當中顯得很混亂。所以,Jetpack提供了一個名爲Navigation的組件,旨在方便開發者管理Fragment頁面和Appbar。android

相比以前Fragment的管理須要藉助FragmentManager和FragmentTransaction,使用Navigation組件有以下一些優勢:web

    • 可視化的頁面導航圖,方便咱們理清頁面之間的關係
    • 經過destination和action完成頁面間的導航
    • 方便添加頁面切換動畫
    • 頁面間類型安全的參數傳遞
    • 經過Navigation UI類,對菜單/底部導航/抽屜藍菜單導航進行統一的管理
    • 支持深層連接DeepLink

    1.2 Navigation元素

    在正式學習Navigation組件以前,咱們須要對Navigation的主要元素有一個簡單的瞭解,Navigation主要由三部分組成。shell

    • Navigation Graph:一個包含全部導航和頁面關係相關的 XML資源。
    • NavHostFragment:一種特殊的Fragment,用於承載導航內容的容器。
    • NavController:管理應用導航的對象,實現Fragment之間的跳轉等操做。

    2、Navigation使用

    2.1 添加依賴

    首先,新建一個Android項目,而後在build.gradle中引入以下依賴腳本。瀏覽器

    dependencies {
      def nav_version = "2.3.2"
      // Java language implementation
      implementation "androidx.navigation:navigation-fragment:$nav_version"
      implementation "androidx.navigation:navigation-ui:$nav_version"
    
      // Kotlin
      implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
      implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
    }

    2.2 建立導航圖

    在【Project】窗口中,res 目錄下右鍵而後依次選擇 【New】->【Android Resource File】建立 New Resource File 對話框,以下圖所示。
    在這裏插入圖片描述
    在彈出的界面中,File name可隨意輸入,Resource type選擇Navigation,而後點擊肯定便可。
    在這裏插入圖片描述
    點擊肯定後,就會在res目錄下建立navigation目錄,以及導航文件nav_graph.xml。安全

    2.3 Navigation graph

    打開新建的nav_graph.xml文件,在Design界面能夠看到目前尚未內容,能夠【依次點擊 New Destination】 圖標,而後點擊【 Create new destination】,便可快速建立新的Fragment,這裏分別新建了FragmentA、FragmentB、FragmentC三個fragment。架構

    在這裏插入圖片描述
    而後,可經過手動配置頁面之間的跳轉關係,點擊某個頁面,右邊會出現一個小圓點,拖曳小圓點指向跳轉的頁面,好比設置跳轉的關係爲FragmentA -> FragmentB -> FragmentC。
    在這裏插入圖片描述
    而後,咱們再切換到Code面板,能夠看到生成的代碼以下所示。app

    <?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/nav_graph"
        app:startDestination="@id/fragmentA">
    
        <fragment
            android:id="@+id/fragmentA"
            android:name="com.example.testnavigation.FragmentA"
            android:label="fragment_a"
            tools:layout="@layout/fragment_a" >
            <action
                android:id="@+id/action_fragmentA_to_fragmentB"
                app:destination="@id/fragmentB" />
        </fragment>
        <fragment
            android:id="@+id/fragmentB"
            android:name="com.example.testnavigation.FragmentB"
            android:label="fragment_b"
            tools:layout="@layout/fragment_b" >
            <action
                android:id="@+id/action_fragmentB_to_fragmentC"
                app:destination="@id/fragmentC" />
        </fragment>
        <fragment
            android:id="@+id/fragmentC"
            android:name="com.example.testnavigation.FragmentC"
            android:label="fragment_c"
            tools:layout="@layout/fragment_c" />
    </navigation>

    上面的生成的代碼中用到了幾個標籤,含義以下。ide

    • navigation:根標籤,經過startDestination配置指定默認的啓動頁面。
    • fragment: fragment標籤表明一個fragment視圖。
    • action:action標籤訂義了頁面跳轉的行爲,destination標籤訂義跳轉的目標頁,跳轉時還能夠定義跳轉動畫。

    2.4 NavHostFragment

    咱們知道,Fragment須要有一個Activity容器才能正常運行,NavHostFragment就是承載導航內容的容器,而且它須要和Activity綁定。工具

    <?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"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <fragment
            android:id="@+id/fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:navGraph="@navigation/nav_graph" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>

    其中, android:name="androidx.navigation.fragment.NavHostFragment"這行代碼的做用就是告訴Android系統這是一個特殊的Fragment。而且當 app:defaultNavHost="true"屬性爲true時,該Fragment會自動處理系統返回。

    • android:name指定NavHostFragment
    • app:navGraph指定導航視圖,即建好的nav_graph.xml
    • app:defaultNavHost=true,能夠攔截系統的返回鍵,能夠理解爲默認給fragment實現了返回鍵的功能。

    而後咱們運行程序,能夠看到默認展現的是FragmentA頁面,這是由於MainActivity的佈局文件中配置了NavHostFragment,而且給NavHostFragment指定了默認展現的頁面爲FragmentA。

    2.5 NavController

    NavController主要用來管理fragment之間的跳轉,每一個 NavHost 均有本身的相應 NavController。能夠經過findNavController來獲取NavController,而後使用NavController的navigate或者navigateUp方法來進行頁面之間的路由操做。

    打開FragmentA.java文件,而後在onViewCreated生命週期方法中添加以下代碼。

    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.fragment_a, container, false);
            Button btnB = view.findViewById(R.id.btn_b);
            btnB.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Navigation.findNavController(v).navigate(R.id.action_fragmentA_to_fragmentB);
                }
            });
            return view;
        }

    運行上面的代碼,而後點擊FragmentA頁面上的按鈕,系統就會經過Navigation.findNavController(v).navigate()方法導航到FragmentB。

    2.6 添加動畫

    在Fragment之間進行跳轉時,還能夠添加跳轉的動畫。打開nav_graph.xml文件的Design選項,而後在Attributes 面板的 Animations 部分中,點擊要添加的動畫旁邊的下拉箭頭,開發者能夠從如下類型中進行選擇,以下圖所示。
    在這裏插入圖片描述
    其中,Attributes面板涉及的屬性含義以下。

    • enterAnim:跳轉時的目標頁面動畫
    • exitAnim: 跳轉時的原頁面動畫
    • popEnterAnim:回退時的目標頁面動畫
    • popExitAnim:回退時的原頁面動畫

    而後,打開Code面板,生成的代碼以下。

    <fragment
            android:id="@+id/fragmentA"
            android:name="com.xzh.jetpack.FragmentA"
            android:label="fragment_a"
            tools:layout="@layout/fragment_a" >
            <action
                android:id="@+id/action_fragmentA_to_fragmentB"
                app:destination="@id/fragmentB"
                app:enterAnim="@android:anim/slide_in_left"
                app:exitAnim="@android:anim/slide_out_right"
                app:popEnterAnim="@anim/fragment_fade_enter"
                app:popExitAnim="@anim/fragment_fade_exit" />
        </fragment>

    3、參數傳遞

    在Android中,頁面之間若是要傳遞數據,建議傳遞最少許的數據,由於在 Android 上用於保存全部狀態的總空間是有限的。若是您須要傳遞大量數據,可使用 ViewModel。

    Fragment的切換常常伴隨着參數的傳遞,爲了配合Navigation組件在切換Fragment時傳遞參數,Android Studio爲開發者提供了Safe Args和Bundle兩種參數傳遞方式。

    3.1 使用Bundle傳遞數據

    使用Bundle傳遞數據時,首先建立 Bundle 對象,而後使用 navigate() 將它傳遞給目的地,以下所示。

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        tv.setOnClickListener {
             Bundle bundle = new Bundle();
             bundle.putString("key", "from fragmentA");
             Navigation.findNavController(v).navigate(R.id.action_fragmentA_to_fragmentB,bundle);
        }
    }

    而後,接受方使用getArguments() 方法來獲取Bundle 傳遞的數據,以下所示。

    String keyStr = getArguments().getString("key");

    3.2 使用 Safe Args傳遞數據

    首先,在項目的 build.gradle 中添加classpath配置,以下所示。

    dependencies {
        def nav_version = "2.3.2"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }

    而後,在app的 build.gradle添加 apply plugin腳本,以下所示。

    apply plugin: 'androidx.navigation.safeargs'

    配置完成後記從新rebuild下項目,會生成{module}/build/generated/source/navigation-args/{debug}/{packaged}/{Fragment}Dircetions,以下所示。
    在這裏插入圖片描述

    若是須要往目的頁面傳遞數據,首先請按照如下步驟將參數添加到接收它的目的頁面中。Navigation提供了一個子標籤argument能夠用來傳遞參數。

    首先,在 Navigation Editor 中,點擊接收參數的目的頁面,在 Attributes 面板中,點擊 Add (+)。而後,在顯示的 Add Argument Link 窗口中,輸入參數名稱、參數類型、參數是否可爲 null,以及默認值(若是須要)點擊 【Add】按鈕,以下所示。

    在這裏插入圖片描述
    點擊 Text 標籤頁切換到 XML 視圖,會發現生成的代碼以下。

    <argument
         android:name="key"
         app:argType="string"
         app:nullable="false"
         android:defaultValue="navigation參數傳遞" />

    而後,咱們在FragmentA.java中使用以下代碼傳遞數據,以下所示。

    @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.fragment_a, container, false);
            Button btnB = view.findViewById(R.id.btn_b);
            btnB.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                  FragmentADirections.ActionFragmentAToFragmentB action=FragmentADirections.actionFragmentAToFragmentB().setKey("經過safeArgs進行參數傳遞");
                  Navigation.findNavController(v).navigate(action);
                }
            });
            return view;
        }

    在接受的頁面的onCreate方法中使用以下的方法進行接受。

    public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            if (getArguments() != null) {
                String name=  FragmentBArgs.fromBundle(getArguments()).getKey();
            }
        }

    4、深層連接DeepLink

    當應用程序接受到某個通知推送,但願用戶在點擊該通知時,可以直接跳轉到展現該通知內容的頁面,這就是深層連接DeepLink最多見的場景,Navigation組件提供了對深層連接(DeepLink)的支持。

    DeepLink有兩種應用場景,一種是PendingIntent,另外一種是真實的URL連接,利用這兩種方式均可以跳轉到程序中指定的頁面。

    4.1 PendingIntent

    PendingIntent方式通常用在消息通知中,當應用程序接收到某個通知時,而且但願用戶在單擊該通知時直接跳轉到到指定的頁面,那麼就能夠經過PendingIntent來完成。

    例如,下面的代碼實現功能是,在MainActivity中單擊按鈕彈出通知欄,點擊通知欄跳轉到指定NotificationActivity頁面中,代碼以下。

    public class MainActivity extends AppCompatActivity {
    
        NotificationManager manager=null;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            init();
        }
    
        private void init() {
            manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
            if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
                //注意通知渠道一旦創建就無權修改
                NotificationChannel channel =new NotificationChannel("normal","Normal",NotificationManager.IMPORTANCE_DEFAULT);
                manager.createNotificationChannel(channel);
                NotificationChannel channel2 =new NotificationChannel("important","Important",NotificationManager.IMPORTANCE_HIGH);
                manager.createNotificationChannel(channel2);
            }
    
            Button pending=findViewById(R.id.btn_pendingintent);
            pending.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    sendNotification();
                }
            });
        }
    
        void sendNotification(){
            Intent intent =new Intent(this,NotificationActivity.class);
            PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);
            //使用NotificationCompat兼容8.0之前的系統
    //        val notification = NotificationCompat.Builder(this,"normal")
            Notification notification = new NotificationCompat.Builder(this,"important")
                    .setContentTitle("This is content title")
                    .setContentText("This is content text")
                    .setStyle(new NotificationCompat.BigTextStyle().bigText("Learn how to build notification,send any sync data,and use voice actions.Get the " +
                            "official Android IDE and developer tools to build apps for Android."))
                    .setStyle(new NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher_background)))
                    .setSmallIcon(R.drawable.ic_launcher_foreground)
                    .setLargeIcon(BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher_background))
                    .setContentIntent(pi)
                    .setAutoCancel(true)
                    .build();
            manager.notify(1,notification);
        }
    }

    而後,須要新建一個通知的目的頁面,爲了MainActivity作區分,咱們在NotificationActivity頁面僅放了一個TextView組件,以下所示。

    public class NotificationActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_notification);
        }
    }

    而後運行上面的代碼,最終的效果以下圖所示。
    在這裏插入圖片描述

    4.2 URL

    使用URL連接方式,當用戶經過手機瀏覽器瀏覽網站上的某個頁面時,能夠經過網頁瀏覽器的方式打開對應的應用頁面。若是用戶的手機安裝有咱們得應用程序,那麼經過DeepLink就能打開相應的頁面;若是沒有安裝,那麼網站能夠導航到應用程序的下載頁面,從而引導用戶安裝應用程序。

    首先,在導航圖中爲destination添加<deepLink/>標籤,在app:uri屬性中填入的是你的網站的相應web頁面地址,以下所示。

    <fragment
        android:id="@+id/deepLinkSettingsFragment"
        android:name="com.michael.deeplinkdemo.DeepLinkSettingsFragment"
        android:label="fragment_deep_link_settings"
        tools:layout="@layout/fragment_deep_link_settings">
    
        <!-- 爲destination添加<deepLink/>標籤 -->
        <deepLink app:uri="www.YourWebsite.com/{params}" />
    
    </fragment>

    如上所示,app:uri裏面填的是網站的相應Web頁面地址,{params}裏面的參數會經過Bundle對象傳遞到頁面中。

    而後,爲相應的Activity設置<nav-graph/>標籤,當用戶在Web中訪問到連接時,你的應用程序便能監聽到,以下所示。

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".DeepLinkActivity">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
    
            <!-- 爲Activity設置<nav-graph/>標籤 -->
           <nav-graph android:value="@navigation/nav_graph"/>
        </activity>
    
    </application>

    通過上面的設置後,接下來就能夠測試咱們的功能了。咱們能夠在Google app中輸入相應的Web地址,也能夠經過adb工具,使用命令行來完成測試操做。

    adb shell am start -a android.intent.action.VIEW -d "http://www.YourWebsite.com/fromWeb"

    執行完命令,手機便能直接打開deepLinkSettingsFragment。在該Fragment中,咱們能夠經過Bundle對象獲取相應的參數(fromWeb),從而完成後續的操做。

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
        View view = inflater.inflate(R.layout.fragment_deep_link_settings, container, false);
        Bundle bundle = getArguments();
        if(bundle != null){
            String params = bundle.getString("params");
            TextView tvDesc = view.findViewById(R.id.tvDesc);
            if(!TextUtils.isEmpty(params)){
                tvDesc.setText(params);
            }
        }
        return view;
    }
    相關文章
    相關標籤/搜索