صفر تا صد آموزش JetPack Navigation در اندروید با مثال عملی

صفر تا صد آموزش JetPack Navigation در اندروید با مثال عملی

اگر شما تا به حال یک برنامه اندرویدی نوشته باشید حتما می دانید که جهت یابی بین اکتیویتی ها و فرگمنت ها چه کار زمان بر و پیچیده ای است و از دردسر های مدیریت BackStack هم کاملا آگاه هستید. اخیرا شرکت گوگل کتابخانه جدیدی به نام Navigation Component از Jet pack را معرفی کرده که یک راه تازه و به مراتب آسان تری برای کنترل جهت یابی در برنامه های اندرویدی پیش روی برنامه نویسان و توسعه دهندگان اندرویدی قرار داده و امکان رسم گرافی مصور از جهت یابی بین صفحات برنامه را فراهم می کند.
در این آموزش قدم به قدم، طی مثالی ساده، هر چیزی را که برای شروع با کتابخانه Android Jetpack Navigation Component به آنها نیاز دارید، یاد می گیریم.
- مطالبی که در این آموزش با آنها آشنا خواهیم شد:
• آشنایی با ساختار کتابخانه Navigation Component
• ساخت یک پروژه با کاتلین
• وابستگی های کتابخانه ی Navigation Component
• ساخت فرگمنت ها
• ساخت یک گراف جهت یابی- Navigation Graph
• فرگمنت میزبان جهت یابی- Navigaton Host Fragment
• افزودن فرگمنت ها به گراف جهت یابی
• NavController و جهت یابی بین فرگمنت ها
• ارسال داده به عنوان Argument توسط Bundle به یک فرگمنت در کتابخانه Navigation
• مدیریت backstack با استفاده از ویژگی های PopUpTo and PopUpToInclusive
• انتقالات انیمیشنی فرگمنت ها با استفاده از گراف جهت یابی

1: آشنایی با کتابخانه Navigation Components

قبل از هرچیز بهتر است مختصرا با ساختار و اجزای  این کتابخانه آشنا شویم.

کتابخانه ی Navigation Components دارای سه بخش اصلی است :

1-    Navigation Graph – گراف جهت یابی

2-    NavHostFragment – فرگمنت میزبان جهت یابی

3-    NavController- کنترل کننده و هماهنگ کننده ی جهت یابی

تمام ساز و کار این کتابخانه توسط این سه بخش اصلی پیاده می شود. آنها را به خاطر بسپارید، با هرکدام به طور کامل در طول مثال آشنا خواهیم شد.

2: ساخت یک پروژه جدید

مسیر File>New>NewProject را دنبال کرده و یک پروژه جدید می سازیم:
تصویر یک: ساخت یک پروژه کاتلین جدید در اندروید استودیو

نکته: برای استفاده از کتابخانه Navigation Components نصب نسخه3.3 اندروید استودیو و یا نسخه بالاتر ضروری است.

3: اضافه کردن dependency های لازم به برنامه

برای پشتیبانی برنامه از کتابخانه Navigation لازم است که وابستگی های مربوطه ی زیر را به فایل

build.gradle(Module:app)

اضافه کنیم:

dependencies {
def nav_version = "2.3.1"

// 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"

توجه: هنگام ارائه ی این آموزش، این نسخه از کتابخانه Navigation، آخرین نسخه ی منتشر شده توسط تیم اندروید شرکت گوگل می باشد.
در برنامه پیش رو علاوه بر وابستگی های Navigation به کتابخانه Material نیز نیاز داریم، پس آن را هم به فایل

build.gradle(Module:app)

اضافه کنید.

def material_version = "1.1.0-alpha07"
implementation "com.google.android.material:material:$material_version"

4: ساخت فرگمنت ها

برنامه دارای چهار فرگمنت است:
MainFragment (به هنگام شروع برنامه نمایش داده می شود.)
EnterNameFragment
EnterNumberFragment
ShowInformationFragment
برای ایجاد فرگمنت ها، روی پکیج برنامه راست کلیک کرده و مسیر

New>Fragment>Fragment(Blank)

را دنبال کنید.

تصویر دو : ساخت یک فرگمنت جدید (MainFragment)

پس از ساخت کلاس فرگمنت های برنامه، یک لایه ی xml به نام هرکدام ساخته می شود. فایل لایه های مربوط به فرگمنت ها همانند لایه ی اکتیویتی برنامه در پوشه منابع (res) در بخش لایه ها (layout) ذخیره می شود.

5: ساخت لایه ها

هدف از این آموزش، یادگیری چگونگی کاربرد کتابخانه NavigationComponents است، به همین جهت لایه ها بسیار ساده طراحی شده اند و مختصرا توضیح داده خواهند شد.

1-لایه ی MainFragment :

لایه فرگمنت اصلی برنامه تنها دارای یک Button است.
توجه : فایل color برنامه در انتهای آموزش آورده شده است.

fragment_main.xml

<?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:id="@+id/parentLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/primary_light"
    tools:context=".MainFragment">
    <Button
        android:id="@+id/send_info_btn"
        android:layout_width="290dp"
        android:layout_height="90dp"
        android:layout_marginStart="20dp"
        android:layout_marginEnd="20dp"
        android:background="@color/divider"
        android:text="Send Your Personality Information"
        android:textColor="@color/primary_text"
        android:textSize="18sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

تصویر سه : MainFragment

2-لایه ی EnterNameFragment:

fragment_enter_name.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="@color/primary_light"
    tools:context=".EnterNameFragment">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintVertical_bias=".25"
        android:orientation="horizontal"
        android:gravity="center_horizontal"
        android:weightSum="100"
        android:baselineAligned="false">

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:errorEnabled="true"
            android:layout_weight="60"

            >

            <com.google.android.material.textfield.TextInputEditText
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:inputType="text"
                android:maxLines="1"
                android:hint="@string/name"
                android:id="@+id/input_name"
                />

        </com.google.android.material.textfield.TextInputLayout>

    </LinearLayout>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintVertical_bias=".4"
        app:layout_constraintRight_toLeftOf="@+id/cancel_btn"
        android:id="@+id/next_btn"
        android:text="@string/next"
        android:background="@color/divider"
        android:textColor="@color/primary_text"
        />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toRightOf="@+id/next_btn"
        app:layout_constraintVertical_bias=".4"
        android:id="@+id/cancel_btn"
        android:text="@string/cancel"
        android:background="@color/divider"
        android:textColor="@color/primary_text"
        />
</androidx.constraintlayout.widget.ConstraintLayout>

تصویر چهار : EnterNameFragment

3-لایه ی EnterNumberFragment:

نکته ای که در این لایه باید به آن توجه کنیم، نوع ورودی EditText می باشد که یک عدد است.

Fragment_enter_number.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout     xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="@color/primary_light"
    tools:context=".EnterNumberFragment">


    <TextView android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintVertical_bias=".15"
        android:textColor="@color/secondary_text"
        android:textSize="15dp"
        android:id="@+id/name"
        />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintVertical_bias=".25"
        android:orientation="horizontal"
        android:gravity="center_horizontal"
        android:weightSum="100">

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:errorEnabled="true"
            android:layout_weight="60"

            >

            <com.google.android.material.textfield.TextInputEditText
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:inputType="numberDecimal"
                android:maxLines="1"
                android:hint="Number"
                android:id="@+id/input_number"
                />

        </com.google.android.material.textfield.TextInputLayout>

    </LinearLayout>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintVertical_bias=".4"
        app:layout_constraintRight_toLeftOf="@+id/cancel_btn"
        android:id="@+id/send_btn"
        android:text="send"
        android:background="@color/divider"
        android:textColor="@color/primary_text"
        />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toRightOf="@+id/send_btn"
        app:layout_constraintVertical_bias=".4"
        android:id="@+id/cancel_btn"
        android:text="cancel"
        android:background="@color/divider"
        android:textColor="@color/primary_text"
  />

</androidx.constraintlayout.widget.ConstraintLayout>

تصویر پنج : EnterNumberFragment

4-لایه ی فرگمنت ShowInformation:

fragment_show_information.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="@color/primary_light"
    tools:context=".ShowInformationFragment">


    <TextView
        android:id="@+id/informatin_message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="This is a default text:"
        android:textColor="@color/primary_text"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHorizontal_bias="0.497"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.367" />

    <TextView
        android:id="@+id/informatin_message2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="SOKAN ACADEMY :)"
        android:textColor="@color/primary_text"
        android:textSize="30sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.541" />


</androidx.constraintlayout.widget.ConstraintLayout>

تصویر شش : ShowInformationFragment

6: ساخت گراف جهت یابی NavGraph

به اولین و جذاب ترین بخش کتابخانه navigation رسیدیم:) ؛ گراف جهت یابی.
فایل گراف جهت یابی، یک فایل xml است که همانند فایل لایه ها دارای بخش Code و Design می باشد و در پوشه منابع برنامه ذخیره می شود. برای ایجاد فایل گراف جهت یابی روی پوشه منابع برنامه راست کلیک کرده و یک فایل resource جدید ایجاد می کنیم.

تصویر هفت : ایجاد یک Android resource fileجدید

در پنجره ای که باز می شود، نام فایل را وارد کرده (nav_graph) و نوع resource را همانند تصویر شماره هشت، "navigation" قرار می دهیم.


تصویر هشت : ایجاد یک گراف جهت یابی به نام nav_graph

مطابق تصویر هشت، اندروید استودیو یک دایرکتوری به نام navigation می سازد که فایل xml گراف، درون آن و در پوشه منابع ذخیره می گردد.
هر گراف جهت یابی مجموعه ای از مقاصد (Destinations) و حرکت ها (Actions) است. در واقع مقصد در گراف برنامه، یک فرگمنت است و اینکه کاربر از هر فرگمنت به فرگمنت های بعدی چه مسیری را طی می کند با action ها مشخص می شود. برای درک بیشتر بهتر است گراف مصور برنامه را بسازیم:

تصویر نه : آیکون افزودن مقصد به گراف جهت یابی

روی آیکون مشخص شده در تصویر شماره نه کلیک کرده و مقاصد (destinatins) گراف را اضافه کنید. به یاد داشته باشید اولین انتخابتان به عنوان مقصد شروع در نظر گفته می شود. پس فرگمنت اصلی، اولین مقصدی است که باید اضافه کنید. با انتخاب هر فرگمنت و سپس کلیک روی آیکون می توانید فرگمنت شروع را تغییر دهید.

تصویر ده : مقصد ها و حرکت ها ی گراف جهت یابی

بعد از اضافه کردن مقاصد، با کلیک روی هر فرگمنت، حرکت مورد نظر را به سمت فرگمنت بعدی بکشید. گراف جهت یابی این برنامه بسیار ساده است و فقط کافیست طبق مسیری که پیموده می شود مقاصد را به هم متصل کنید. بعد از اتمام ترسیم گراف به بخش codeفایل بروید. مشاهده می کنید؟! به ازای هر فرگمنت، یک تگ fragment با Id و نام مشخص و یک تگ action با Id و مقصد بعدی مربوط به آن حرکت ساخته شده است.
اگر توجه کنید تگ navigation اعلام خطا می کند. در حال حاضر ما گراف جهت یابی برنامه را مشخص کردیم ولی می دانیم که فرگمنت ها به تنهایی قابلیت اجرا شدن ندارند و همگی به اکتیویتی وابسته اند، این هشدار به این علت است که ما هنوز یک اکتیویتی را به عنوان میزبان این گراف مشخص نکرده ایم. اگر در بخش Design هم روی Destination کلیک کنید جای Host هیچ میزبانی مشخص نشده است. در گام بعد با دومین بخش از کتابخانه Navigation یعنی NavHost آشنا می شویم.

7: تعیین میزبان فرگمنت های جهت یابی – NavHostFragment

برای تعیین میزبان، به لایه ی اکتیویتی برنامه می رویم که هنوز آن را طراحی نکرده ایم. اکتیوتی برنامه قرار است میزبان تک تک فرگمنت های گراف باشد و تنها دارای یک تگ "فرگمنت" است که آن فرگمنت همان NavHostFragment نامیده می شود.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".MainActivity">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />
                
</androidx.constraintlayout.widget.ConstraintLayout>

برای اینکه یک "تگ فرگمنت معمولی" به" میزبان یک گراف جهت یابی" تبدیل شود تنها به سه جمله کد نیاز است:
1- شما باید مشخص کنید که میزبان گراف (NavHost) از کلاس Fragment است.

android:name="androidx.navigation.fragment.NavHostFragment"

2- تنها یک NavHostمی تواند به عنوان میزبان پیش فرض جهت یابی باشد پس شما باید تعیین کنید این تگ به عنوان میزبان پیش فرض است. اگر خصیصه ی defaultNavHost را برابر true قرار بدهید میزبانی که انتخاب کردید اجازه خروج با دکمه Back را نمی دهد.

app:defaultNavHost="true"

3- و در آخر لازم است گراف جهت یابی که ساخته اید را به این میزبان معرفی کنید.

app:navGraph="@navigation/nav_graph"

8: ساخت شی از کلاس NavController و جابه جایی در گراف

بعد از ساخت NavGraph و تعیین NavHost برنامه، هنوز نمی توانیم بین فرگمنت ها جابه جا شویم. حالا باید ببینیم کتابخانه Navigation چگونه می تواند کاربر را از مقصدی به مقصد دیگر جابه جا کند؟
هر NavHost دارای NavController مخصوص به خودش می باشد و به NavGraph نیز دسترسی دارد و هر NavController برای جهت یابی از یک مقصد به مقصد دیگر استفاده می شود.
با فرگمنت اصلی برنامه شروع می کنیم:
فرگمنت اصلی اولین مقصد در گراف جهت یابی است و در جایگاه "نگهدارنده فرگمنت"موجود در لایه ی اکتیویتی (میزبان جهت یابی) قرار می گیرد و توسط NavController مخصوص خود به مقصد بعدی جابه جا می شود. برای اینکه از فرگمنت اصلی به مقصد بعدی جابه جا شویم می توانیم از یکی از متد های زیر استفاده کنیم که NavController فرگمنت مورد نظر را بازیابی کند:

• NavHostFragment.findNavController(Fragment)

• Navigation.findNavController(Activity, @IdRes int viewId)

• Navigation.findNavController(View)

از کلاس NavController یک شی می سازیم و آن را در کلاس فرگمنت اصلی تعریف کرده و در متد ()onViewCreated آن را برابر NavController بازیابی شده قرار می دهیم.

lateinit var navController: NavController 
navController = Navigation.findNavController(view)

نکته : همانطور که می دانید در زبان کاتلین، به غیر از زمانی که اعلام کنید شی تعریف شده nullable است، اجازه ندارید شی را با null مقدار بدهید. علت استفاده از کلمه ی lateinit که از late-initializedگرفته شده هم به این خاطر است که نشان دهیم ما قصد مقدار دهی به این شی را داریم اما کمی دیرتر!!
بعد از تعریف و مقداردهی شی از کلاس NavController، نوبت این است که Button(view) را با متد ()findViewById شناسایی کرده و تابع onClick آن را بنویسیم. برای نوشتن تابع onClick در واقع قرار است با استفاده از متد navigate از کلاس NavController مشخص کنیم که کاربر پس از کلیک بر روی Button به چه جهت و مسیری هدایت شود:

override fun onClick(v: View?) {
when (v!!.id) {
R.id.send_info_btn -> navController!!.navigate(R.id.action_mainFragment_to_enterNameFragment)

پارامتر متد navigate می تواند" Id حرکت به مقصد بعدی" (action) و یا" Id مقصد بعدی"(destination) باشد. بعد از MainFragment، مقصد بعدی در گراف EnterNameFragment است پس Id مربوط به این حرکت را در پارامتر متد ()navigate می نویسیم.

به این ترتیب فایل کاتلین فرگمنت اصلی به صورت زیر است:

MainFragment.kt

package com.example.navigationcomponentsexample
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.navigation.NavController
import androidx.navigation.Navigation

class MainFragment : Fragment(),View.OnClickListener {
    lateinit var navController: NavController

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_main, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        navController = Navigation.findNavController(view)
        view.findViewById<Button>(R.id.send_info_btn).setOnClickListener(this)

    }

    override fun onClick(v: View?) {
        when (v!!.id) {
            R.id.send_info_btn ->
   navController!!.navigate(R.id.action_mainFragment_to_enterNameFragment)
        }
    }
}

9: انتقال داده از مقصدی به مقصد دیگر به عنوان Argument با NavController

تا این مرحله ما می توانیم از MainFragment به EnterNameFragment جابه جا شویم. این فقط یک "جابه جایی" است!! در حالی که برای جابه جا شدن از EnterNameFragment به EnterNumberFragment قرار است علاوه بر "جابه جایی"، نام کاربر را هم به عنوان یک داده برای نمایش درEnterNumberFragment به آنجا بفرستیم ( جابه جا کنیم!) و همین اتفاق در جابه جایی از EnterNumberFragment به ShowInformationFragment، با انتقال نام و شماره ی کاربر می افتد.
در قدم اول به قسمت کد گراف جهت یابی می رویم. برای دریافت داده توسط فرگمنت ها باید در تگ مربوط به فرگمنت هایی که داده را "دریافت" می کنند، تگ argument را بنویسیم. در این برنامه تنها دو فرگمنت هستند که داده دریافت می کنند: EnterNumberFragment وShowInformationFragment.

<fragment
        android:id="@+id/enterNumberFragment"
        android:name="com.example.navigationcomponentsexample.EnterNumberFragment"
        android:label="fragment_enter_number"
        tools:layout="@layout/fragment_enter_number" >

        <argument android:name="name"
            android:defaultValue="None"/>

        <action
            android:id="@+id/action_enterNumberFragment_to_showInformationFragment"
            app:destination="@id/showInformationFragment"
                   />
 </fragment>

android:name در تگ argument همان نامی است که ما داده را با آن توسط Bundle از EnterNameFragment به EnterNumberFragment می فرستیم. داده ای که قرار است جابه جا شود "نام" کاربر است پس بهتر است android:name همان "name" باشد. مقدار پیش فرض را نیز مسلما برابر " None" قرار می دهیم.
حالا زمان تکمیل فایل EnterNameFragment.kt است. داده (نام کاربر) قرار است از این فرگمنت به EnterNumberFragment فرستاده شود.
مشابه فرگمنت اصلی، شیئی از کلاس NavController ساخته و آن را مقدار دهی می کنیم تا با استفاده از آن جابه جا شویم. در متد ()onClick مربوط به Button ها، هنگام عملیات جابه جایی، متنی که از EditText دریافت کرده ایم را نیز برای فرگمنت بعدی می فرستیم.

EnterNameFragment.kt:

package com.example.navigationcomponentsexample

import android.os.Bundle
import android.text.TextUtils
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.navigation.NavController
import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.fragment_enter_name.*


class EnterNameFragment : Fragment(),View.OnClickListener {
    lateinit var navController: NavController


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_enter_name, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        navController = Navigation.findNavController(view)
        view.findViewById<Button>(R.id.next_btn).setOnClickListener(this)
        view.findViewById<Button>(R.id.cancel_btn).setOnClickListener(this)
    }

    override fun onClick(v: View?) {
        when(v!!.id){
            R.id.next_btn -> {
                if(!TextUtils.isEmpty(input_name.text.toString())){
                    val bundle = bundleOf("name" to input_name.text.toString())
                    navController!!.navigate(
                        R.id.action_enterNameFragment_to_enterNumberFragment,
                        bundle
                    )
                }
                else{
                    Toast.makeText(activity, "Enter a name",
                                   Toast.LENGTH_SHORT).show()
                }
            }

            R.id.cancel_btn -> requireActivity().onBackPressed()
        }
    }
}

کلمه when در کاتلین مشابه switch در جاوا است و Id دکمه ها همان Case ها هستند. در Case مربوط به next_btn با شرط if، در صورتی که متن دریافت شده از EditText خالی نباشد، آن را توسط bundleOf با کلید "name" ( همان نامی که در تگ argument مشخص کردیم.) برای فرگمنت بعدی ارسال می کنیم. برای ارسال تنها کافیست در متد navigate به همراه id حرکت، bundle را نیز به همان جهت بفرستیم. مقدار bundle در EnterNumberFragment دریافت خواهد شد. در صورتی که متنی در EditText وارد نشده باشد، پیغام دلخواهی را برای اطلاع به کاربر Toast می کنیم. در Case مربوط به cancel_btn نیز متد onBackPressed() را صدا زده تا کاربر به عقب بازگردد.
حالا باید به سراغ EnterNumberFragment برویم:
در این فرگمنت قرار است نام کاربر را با کلید "name" از Argument دریافت کنیم. پس در کلاس فرگمنت متغیر رشته ای به نام name تعریف کرده و مقدار آن را در متد ()onCreate برابر داده ی رشته ای که توسط Argument دریافت کرده ایم، قرار می دهیم.

class EnterNumberFragment : Fragment(),View.OnClickListener {
    lateinit var navController: NavController
    lateinit var name: String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        name = requireArguments().getString("name").toString()
    }

بایستی از نام کاربر برای نمایش جمله ای توسط TextView استفاده کنیم. به فرض اگر کاربر نام USER SOKAN را انتخاب کرده باشد، این نام در EnterNumberFragment دریافت می شود و در جمله ای تحت عنوان " SOKAN USER, Please Enter Your Number" به نمایش در می آید.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        navController = Navigation.findNavController(view)
        view.findViewById<Button>(R.id.send_btn).setOnClickListener(this)
        view.findViewById<Button>(R.id.cancel_btn).setOnClickListener(this)
        val message = " $name , please enter your number"
        view.findViewById<TextView>(R.id.name).text = message
    }

برای استفاده از نام دریافتی، یک متغیر به اصطلاح Read-only با نام messageتعریف کرده و در آن از متغییر name ای که در onCreate() مقدار دهی کرده بودیم به این شکل استفاده می کنیم:

val message = " $name , please enter your number"

سپس متن مربوط به TextView این فرگمنت را برابر با message قرار می دهیم. تا این مرحله توانستیم از داده- ی دریافتی استفاده کنیم. حالا باید داده ی "name" را به همراه شماره ی کاربر برای نمایش به فرگمنت آخر برنامه بفرستیم. از آنجایی که ShowInformationFragment همانند EnterNumberFragment "داده" دریافت می کند، بایستی تگ argument را برای این فرگمنت نیز بنویسیم. روند ارسال داده ی name"" همانند قبل است اما برای ارسال و دریافت شماره کاربر، از یک کلاس کمکی استفاده می کنیم تا مقدار "Number" ای که توسط bundle ارسال می‌ ‌شود را مدیریت کنیم، زیرا بهترین روش و بهترین ساختار داده ای برای ذخیره ی مقداری همانند شماره و یا پول استفاده از یک BigDecimal است. این کلاس قرار است کلاس Parcelable را implements کند. کلاس Parcelable یک Interface (رابط) مختص اندروید است و برای نوشتن و خواندن اطلاعات استفاده می شود. فایلی با نام Number در پکیج برنامه می سازیم و کلاس Number را در آن تعریف می کنیم:

Number.kt

package com.example.navigationcomponentsexample

import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import java.math.BigDecimal

@Parcelize
data class Number(val amount: BigDecimal) : Parcelable {}

حالا با استفاده از این رابط کمکی می توانیم شماره ی کاربر را از فرگمنتی ارسال و در فرگمنت دیگر دریافت کنیم. به گراف جهت یابی می رویم تا دو تگ argument را برای showInformationFragment بنویسیم:

<fragment
        android:id="@+id/showInformationFragment"
        android:name="com.example.navigationcomponentsexample.ShowInformationFragment"
        android:label="fragment_show_information"
        tools:layout="@layout/fragment_show_information" />

    <argument android:name="name"
        android:defaultValue="None"/>

    <argument android:name="number"
        app:argType="com.example.navigationcomponentsexample.Number"/>

در تگ argument مربوط به شماره، پکیج و کلاس number را ارجاع می دهیم و argTypeرا برابر با همان کلاس کمکی number قرار می دهیم. حالا که مقدمات دریافت نام و شماره را فراهم کردیم به فایل کاتلین EnterNumberFragment باز می گردیم تا آن را تکمیل کنیم. مطابق فرگمنت قبلی تابع ()onClick دکمه را نوشته و با استفاده از متد navigate جهت یابی و ارسال داده ها را انجام می دهیم:

override fun onClick(v: View?) {
        when(v!!.id){
            R.id.send_btn -> {
                if(!TextUtils.isEmpty(input_number.text.toString())){
                    val number = Number(BigDecimal(input_number.text.toString()))
                    val bundle = bundleOf(
                        "name" to name,
                        "number" to number
                    )
                    navController!!.navigate(
                        R.id.action_enterNumberFragment_to_showInformationFragment,
                        bundle
                    )
                }
                else{
                    Toast.makeText(activity, "Enter your number",
                     Toast.LENGTH_SHORT).show()
                }
            }
            R.id.cancel_btn -> requireActivity().onBackPressed()
        }
    }

همانند قبل برای دکمه send تعیین می کنیم که اگر شماره ای از TextView دریافت شده و آن خالی نیست، یک متغیر به نام number تعریف کرده و آن را برابر با شیئی از کلاس number که مقدار BigDecimal می گیرد، قرا می دهیم. حالا مقادیر name وnumber را با کلید هایی که در تگ Argument تعریف کردیم، توسطbundle ارسال می کنیم. باقی مراحل نیز همانند قبل است.

EnterNumberFragment.kt:

package com.example.navigationcomponentsexample

import android.os.Bundle
import android.text.TextUtils
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.navigation.NavController
import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.fragment_enter_name.*
import kotlinx.android.synthetic.main.fragment_enter_number.*
import java.math.BigDecimal

class EnterNumberFragment : Fragment(),View.OnClickListener {
    lateinit var navController: NavController
    lateinit var name: String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
       name = requireArguments().getString("name").toString()


    }


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_enter_number, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        navController = Navigation.findNavController(view)
        view.findViewById<Button>(R.id.send_btn).setOnClickListener(this)
        view.findViewById<Button>(R.id.cancel_btn).setOnClickListener(this)
        val message = " $name , please enter your number"
        view.findViewById<TextView>(R.id.name).text = message
    }

    override fun onClick(v: View?) {
        when(v!!.id){
            R.id.send_btn -> {
                if(!TextUtils.isEmpty(input_number.text.toString())){
                    val number = Number(BigDecimal(input_number.text.toString()))
                    val bundle = bundleOf(
                        "name" to name,
                        "number" to number
                    )
                    navController!!.navigate(
                        R.id.action_enterNumberFragment_to_showInformationFragment,
                        bundle
                    )
                }
                else{
                    Toast.makeText(activity, "Enter your number", Toast.LENGTH_SHORT).show()
                }
            }

            R.id.cancel_btn -> requireActivity().onBackPressed()
        }
    }
} 

باتوجه به مراحلی که یاد گرفتید، در حال حاضر می توانید کدهای مربوط به ShowInformationFragment را بنویسید. این فرگمنت آخرین فرگمنت برنامه است پس نیازی به NavController برای جهت یابی نداریم. کافیست دو داده ی "name" و number"" را دریافت کرده و در TextView نمایش دهیم:

ShowInformationFragment.kt:

package com.example.navigationcomponentsexample
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView

class ShowInformationFragment : Fragment() {

    lateinit var name: String
    lateinit var number:Number

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        name = requireArguments().getString("name").toString()
        number = requireArguments().getParcelable("number")!!

    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_show_information, container,       false)
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val number = number!!.amount
        val informationMessage = "DEAR $name,YOUR NUMBER IS:"
        val informationMessage2 = "$number"
        view.findViewById<TextView>(R.id.informatin_message).text =   
        informationMessage
        view.findViewById<TextView>(R.id.informatin_message2).text = 
        informationMessage2
    }    }                      

توجه داشته باشید مقدار number دریافتی از نوع parcelable است.

10: فایل های Colors،Strings و Styles برنامه

برای انتخاب رنگ های دلخواه خودتان می توانید از وبسایت Material Design Palette کمک بگیرید.

Colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary">#03A9F4</color>
<color name="primary_dark">#0288D1</color>
<color name="primary_light">#B3E5FC</color>
<color name="accent">#03A9F4</color>
<color name="primary_text">#212121</color>
<color name="secondary_text">#757575</color>
<color name="icons">#FFFFFF</color>
<color name="divider">#BDBDBD</color>
</resources>

Strings.xml

<resources>
    <string name="app_name">NavigationComponentsExample</string>
    <!-- TODO: Remove or change this placeholder text -->
    <string name="hello_blank_fragment">Hello blank fragment</string>
    <string name="send_your_info">Send Your Info</string>
    <string name="view_fragment_a">View Fragment A</string>
    <string name="name">Name</string>
    <string name="next">next</string>
    <string name="cancel">cancel</string>
</resources>

Styles.xml

<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/primary</item>
        <item name="colorPrimaryDark">@color/primary_dark</item>
        <item name="colorAccent">@color/accent</item>
    </style>
</resources>

گیف اجرای برنامه :

شما موفق شدید یک برنامه اندرویدی را با استفاده از کتابخانه NavigationComponent پیاده سازی کنید در قسمت بعدی آموزش، با " انتقالات انیمیشنی بین فرگمنت ها" و مدیریت BackStack برنامه توسط ویژگی هایPopUpTo و PopUpToInclusive آشنا خواهیم شد.

تا اینجا یک مثال جامع با مقدمات کاربرد کتابخانه Navigation Components آشنا شدیم و دریافتیم چگونه می توان با استفاده از سه بخش اصلی این کتابخانه، نه تنها بین فرگمنت های برنامه جابه جا شد بلکه داده نیز جابه جا کرد.
در ادامه قصد داریم با یکی از کلیدی ترین کاربرد های کتابخانه Navigation Components؛ مدیریت BackStack آشنا شویم و همچنین چگونگی افزودن انیمیشن به انتقالات فرگمنت ها را یاد بگیریم.

مدیریت BackStack با PopUpTo و PopUpToInclusive

اندروید یک BackStack را نگهداری می کند که شامل مقاصدی است که شما از آنها بازدید کرده‌اید. اولین مقصد برنامه شما زمانی رویStack (پشته) قرار می‌گیرد که کاربر برنامه را باز می‌کند. به ازای هر فراخوانی متد ()navigate یک مقصد دیگر در بالای Stack قرار می‌گیرد.

یکی از مزایای مهم استفاده از کتابخانه Components Navigation، قابلیت مدیریت آسان BackStack برنامه است. به طور مثال فرض کنید صفحه ی لاگینی را در برنامه خود طراحی کرده اید. کاربر پس از ورود به برنامه اگر روی دکمه back کلیک کند، نباید مجددا به صفحه لاگین باز گردد بلکه بهتراست از برنامه خارج شود. برای حل این مشکل از دو ویژگی PopBehavior به نام های PopUpTo و PopUpToInclusive در تگ action استفاده می کنیم. این دو مفهوم را در مثال برنامه پیاده می کنیم:
به فرض می خواهیم کاربر بعد از پیمودن گراف و رسیدن به آخرین فرگمنت با دکمه back به عقب باز نگردد و از برنامه خارج شود. این یعنی می خواهیم زمانی که به ShowInformationFragment رسیدیم و روی دکمه back کلیک کردیم backstack برنامه پاک شود و زمانی که به پشت سر نگاه می کنیم فرگمنت های قبلی را نبینیم. برای این کار بایستی در تگ action مربوط به EnterNumberFragment، دو ویژگی PopUpTo:MainFragment و PopUpToInclusive:true را اضافه کنیم. ویژگی اول به این معناست که اگر کاربری با دکمه back قصد داشت به EnterNumberFragment بازگردد آن را به MainFragment پرتاپ کن و ویژگی دوم حکم خروج از برنامه را بعد از پرتاپ به فرگمنت اصلی صادر می کند:

<fragment
        android:id="@+id/enterNumberFragment"
        android:name="com.example.navigationcomponentsexample.EnterNumberFragment"
        android:label="fragment_enter_number"
        tools:layout="@layout/fragment_enter_number" >
        <argument android:name="name"
            android:defaultValue="None"/>
        <action
            android:id="@+id/action_enterNumberFragment_to_showInformationFragment"
            app:destination="@id/showInformationFragment"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popUpTo="@id/mainFragment"
            app:popUpToInclusive="true"
           />
</fragment>

گیف قبل از ویژگی PopBehavior :

گیف بعد از ویژگی PopBehavior:


انتقالات انیمیشنی بین فرگمنت ها

برای اینکه بتوانیم با جلوه ی بهتری بین فرگمنت های برنامه جابه جا شویم می توانیم از انیمیشن ها استفاده کنیم. روی پوشه منابع برنامه راست کلیک کرده و یک دایرکتوری به نام anim بسازید:

تصویر11: ساخت یک directory جدید به نام anim

پس از ساخت پوشه anim، روی آن راست کلیک کرده و به تعداد فایل های انیمیشنی؛ Animation Resource بسازید و کد های xml انیمیشن دلخواه خودتان را داخل این فایل ها قرار دهید. به عنوان مثال می توانید از فایل های زیر استفاده کنید:

Fade_in.xml

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

Fade_out.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromAlpha="1.0"
        android:toAlpha="0.0"/>
</set>

Slide_in_left.xml

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

    <translate android:fromXDelta="-100%" android:toXDelta="0%"
        android:fromYDelta="0%" android:toYDelta="0%"
        android:duration="400"/>
</set>

Slide_in_right.xml

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

    <translate android:fromXDelta="100%" android:toXDelta="0%"
        android:fromYDelta="0%" android:toYDelta="0%"
        android:duration="400"/>

</set>

Slide_out_left.xml

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

    <translate android:fromXDelta="0%" android:toXDelta="-100%"
        android:fromYDelta="0%" android:toYDelta="0%"
        android:duration="400"/>
</set>

Slide_out_right.xml

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

    <translate android:fromXDelta="0%" android:toXDelta="100%"
        android:fromYDelta="0%" android:toYDelta="0%"
        android:duration="400"/>   
</set>

بعد از ساخت فایل های انیمیشن حالا کافیست در گراف جهت یابی برای هر action انیمیشن دلخواه را اضافه کنیم. برای مثال تگ action فرگمنت اصلی را به این صورت می نویسیم:

<action
            android:id="@+id/action_mainFragment_to_enterNameFragment"
            app:destination="@id/enterNameFragment"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
          /> 

enterAnim و exitAnim انیمیشن های ورود و خروج  فرگمنت هاست در حالی که popEnterAnim و popExitAnim انیمیشن مربوط به زمانی است که کاربر با استفاده از دکمه back از فرگمنتی خارج و وارد فرگمنت دیگری می شود.

برای یادگیری هر چه بهتر آموزش، سعی کنید پروژه های جدیدی بسازید و برنامه های مختلفی را  این بار با کتابخانه Navigation Components بنویسید که دارای گراف جهت یابی پیچیده تر و قابلیت های  بیشتری باشند. همچنین می توانید برای اطلاع از خبرهای رسمی مربوط به این کتابخانه و کسب اطاعات بیشتر به وبسایت https://developer.android.com/ سر بزنید.

امیدوارم این آموزش برای شما مفید واقع شده باشد. پیروز باشید.

از بهترین نوشته‌های کاربران سکان آکادمی در سکان پلاس


online-support-icon