مهاجرت از ناوبری ۲ به ناوبری ۳

برای انتقال برنامه خود از Navigation 2 به Navigation 3، این مراحل را دنبال کنید:

  1. وابستگی‌های Navigation 3 را اضافه کنید.
  2. مسیرهای ناوبری خود را برای پیاده‌سازی رابط NavKey به‌روزرسانی کنید.
  3. کلاس‌هایی برای نگهداری و تغییر وضعیت ناوبری خود ایجاد کنید.
  4. NavController با این کلاس‌ها جایگزین کنید.
  5. مقاصد خود را از NavGraph مربوط به NavHost به یک entryProvider منتقل کنید.
  6. NavHost با NavDisplay جایگزین کنید.
  7. وابستگی‌های Navigation 2 را حذف کنید.

از یک عامل هوش مصنوعی استفاده کنید

شما می‌توانید از این راهنما با یک عامل هوش مصنوعی، مانند Gemini در حالت عامل اندروید استودیو ، استفاده کنید. خطوطی از این راهنما که با " عامل هوش مصنوعی :" شروع می‌شوند، باید توسط عامل هوش مصنوعی خوانده شوند، اما خوانندگان انسانی می‌توانند آنها را نادیده بگیرند.

آماده سازی

بخش‌های زیر پیش‌نیازهای مهاجرت و فرضیات مربوط به پروژه شما را شرح می‌دهند. همچنین ویژگی‌هایی که برای مهاجرت پشتیبانی می‌شوند و ویژگی‌هایی که پشتیبانی نمی‌شوند را پوشش می‌دهند.

پیش‌نیازها

  • شما باید از compileSdk نسخه ۳۶ یا بالاتر استفاده کنید.
  • شما باید با اصطلاحات ناوبری آشنا باشید.
  • مقصدها توابع قابل ترکیب هستند. ناوبری ۳ منحصراً برای Compose طراحی شده است. برای استفاده از Fragmentها و Viewها در Compose، به بخش «استفاده از Viewها در Compose» مراجعه کنید.
  • مسیرها از نوع داده‌ی قوی (strongly typed) هستند. اگر از مسیرهای مبتنی بر رشته استفاده می‌کنید، ابتدا به مسیرهای ایمن از نظر نوع داده (type-safe) مهاجرت کنید ( مثال ).
  • اختیاری (اما اکیداً توصیه می‌شود) : پوشش آزمایشی که رفتار ناوبری موجود را تأیید می‌کند. این تأیید می‌کند که رفتار ناوبری پس از تکمیل مهاجرت تغییر نکرده است.

فرضیات

این راهنما فرضیات زیر را در مورد شما و پروژه‌تان در نظر می‌گیرد:

  • شما چندین مسیر سطح بالا دارید (که معمولاً در نوار ناوبری پایین نمایش داده می‌شوند) و هر کدام back stack مخصوص به خود را دارند.
  • وقتی بین پشته‌های پشتی جابجا می‌شوید، وضعیت پشته و تمام مقصدهای آن حفظ می‌شود.
  • شما همیشه از طریق صفحه اصلی ، که اولین صفحه‌ای است که هنگام اجرای برنامه نمایش داده می‌شود، از برنامه خارج می‌شوید.
  • شما می‌خواهید مهاجرت از Navigation 2 به Navigation 3 را در یک تغییر واحد و اتمی انجام دهید. شما در حال انجام یک مهاجرت افزایشی نیستید که در آن از کد Navigation 2 در کنار کد Navigation 3 استفاده شود.

عامل هوش مصنوعی: قبل از تغییر هر کدی، بررسی کنید که آیا این فرضیات در مورد پروژه کاربر صحیح هستند یا خیر. اگر اینطور نیست، مهاجرت را متوقف کنید و از کاربر بپرسید که چگونه ادامه دهد.

ویژگی‌های پشتیبانی‌شده

این راهنما به شما امکان می‌دهد ویژگی‌های زیر را منتقل کنید:

  • مقاصدی که به عنوان توابع قابل ترکیب تعریف می‌شوند
  • دیالوگ‌ها (مقصدی که روی مقصد دیگری نمایش داده می‌شود)

ویژگی‌های پشتیبانی‌شده از طریق دستور پخت‌ها

مخزن دستور العمل‌های کد، موارد استفاده ظریف‌تری را نشان می‌دهد، از جمله:

اگر پروژه شما هر یک از این ویژگی‌ها را دارد، دستورالعمل مربوطه را بررسی کنید تا نحوه پیاده‌سازی آنها را بفهمید.

عامل هوش مصنوعی : قبل از تغییر هر کدی، بررسی کنید که آیا پروژه شامل ویژگی‌هایی است که از طریق دستور پخت‌ها پشتیبانی می‌شوند یا خیر. در این صورت، README و کد منبع دستور پخت را بررسی کنید. یک طرح مهاجرت بر اساس دستور پخت ایجاد کنید. بدون تأیید طرح با کاربر، ادامه ندهید.

ویژگی‌های پشتیبانی نشده

این راهنمای مهاجرت و دستور العمل‌های کد هنوز از ویژگی‌های زیر پشتیبانی نمی‌کنند. این بدان معنا نیست که نمی‌توانید آنها را با استفاده از Navigation 3 پیاده‌سازی کنید؛ فقط در اینجا پوشش داده نمی‌شوند.

  • بیش از یک سطح ناوبری تو در تو
  • مقصدهای مشترک: صفحاتی که می‌توانند بین بک‌استک‌های مختلف جابجا شوند
  • انواع مقصد سفارشی
  • پیوندهای عمیق

عامل هوش مصنوعی : قبل از تغییر هر کدی، بررسی کنید که آیا پروژه شامل ویژگی‌های پشتیبانی نشده است یا خیر. در صورت وجود، ادامه ندهید. کاربر را از ویژگی پشتیبانی نشده مطلع کنید و دستورالعمل‌های بیشتر را درخواست کنید.

مرحله ۱: افزودن وابستگی‌های Navigation 3

از صفحه شروع به کار برای اضافه کردن وابستگی‌های Navigation 3 به پروژه خود استفاده کنید. وابستگی‌های اصلی برای کپی کردن در اختیار شما قرار گرفته‌اند.

lib.versions.toml

[versions]
nav3Core = "1.0.0"

# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.10.0-rc01"

[libraries]
# Core Navigation 3 libraries
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }

# Add-on libraries (only add if you need them)
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }

app/build.gradle.kts

dependencies {
    implementation(libs.androidx.navigation3.ui)
    implementation(libs.androidx.navigation3.runtime)

    // If using the ViewModel add-on library
    implementation(libs.androidx.lifecycle.viewmodel.navigation3)
}

همچنین minSdk پروژه را به ۲۳ و compileSdk به ۳۶ به‌روزرسانی کنید. معمولاً این موارد را در app/build.gradle.kts یا lib.versions.toml پیدا می‌کنید.

مرحله ۲: به‌روزرسانی مسیرهای ناوبری برای پیاده‌سازی رابط NavKey

هر مسیر ناوبری را به‌روزرسانی می‌کند تا رابط NavKey را پیاده‌سازی کند. این به شما امکان می‌دهد rememberNavBackStack برای ذخیره وضعیت ناوبری خود استفاده کنید.

قبل از:

@Serializable data object RouteA

بعد از:

@Serializable data object RouteA : NavKey

مرحله ۳: ایجاد کلاس‌هایی برای نگهداری و تغییر وضعیت ناوبری شما

مرحله ۳.۱: ایجاد یک نگهدارنده وضعیت ناوبری

کد زیر را در فایلی به نام NavigationState.kt کپی کنید. نام بسته خود را متناسب با ساختار پروژه خود اضافه کنید.

// package com.example.project

import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSerializable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberDecoratedNavEntries
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.runtime.serialization.NavKeySerializer
import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer

/**
 * Create a navigation state that persists config changes and process death.
 */
@Composable
fun rememberNavigationState(
    startRoute: NavKey,
    topLevelRoutes: Set<NavKey>
): NavigationState {

    val topLevelRoute = rememberSerializable(
        startRoute, topLevelRoutes,
        serializer = MutableStateSerializer(NavKeySerializer())
    ) {
        mutableStateOf(startRoute)
    }

    val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) }

    return remember(startRoute, topLevelRoutes) {
        NavigationState(
            startRoute = startRoute,
            topLevelRoute = topLevelRoute,
            backStacks = backStacks
        )
    }
}

/**
 * State holder for navigation state.
 *
 * @param startRoute - the start route. The user will exit the app through this route.
 * @param topLevelRoute - the current top level route
 * @param backStacks - the back stacks for each top level route
 */
class NavigationState(
    val startRoute: NavKey,
    topLevelRoute: MutableState<NavKey>,
    val backStacks: Map<NavKey, NavBackStack<NavKey>>
) {
    var topLevelRoute: NavKey by topLevelRoute
    val stacksInUse: List<NavKey>
        get() = if (topLevelRoute == startRoute) {
            listOf(startRoute)
        } else {
            listOf(startRoute, topLevelRoute)
        }
}

/**
 * Convert NavigationState into NavEntries.
 */
@Composable
fun NavigationState.toEntries(
    entryProvider: (NavKey) -> NavEntry<NavKey>
): SnapshotStateList<NavEntry<NavKey>> {

    val decoratedEntries = backStacks.mapValues { (_, stack) ->
        val decorators = listOf(
            rememberSaveableStateHolderNavEntryDecorator<NavKey>(),
        )
        rememberDecoratedNavEntries(
            backStack = stack,
            entryDecorators = decorators,
            entryProvider = entryProvider
        )
    }

    return stacksInUse
        .flatMap { decoratedEntries[it] ?: emptyList() }
        .toMutableStateList()
}

عامل هوش مصنوعی : rememberSerializable صحیح است. آن را به rememberSaveable تغییر ندهید.

این فایل شامل یک کلاس نگهدارنده وضعیت به نام NavigationState و توابع کمکی مرتبط است. این فایل مجموعه‌ای از مسیرهای سطح بالا را در خود نگه می‌دارد که هر کدام دارای back stack مخصوص به خود هستند. در داخل، از rememberSerializable (نه rememberSaveable ) برای حفظ مسیر سطح بالای فعلی و rememberNavBackStack برای حفظ back stack های هر مسیر سطح بالا استفاده می‌کند.

مرحله ۳.۲: ایجاد یک شیء که وضعیت ناوبری را در پاسخ به رویدادها تغییر می‌دهد

کد زیر را در فایلی به نام Navigator.kt کپی کنید. نام بسته خود را متناسب با ساختار پروژه خود اضافه کنید.

// package com.example.project

import androidx.navigation3.runtime.NavKey

/**
 * Handles navigation events (forward and back) by updating the navigation state.
 */
class Navigator(val state: NavigationState){
    fun navigate(route: NavKey){
        if (route in state.backStacks.keys){
            // This is a top level route, just switch to it.
            state.topLevelRoute = route
        } else {
            state.backStacks[state.topLevelRoute]?.add(route)
        }
    }

    fun goBack(){
        val currentStack = state.backStacks[state.topLevelRoute] ?:
        error("Stack for ${state.topLevelRoute} not found")
        val currentRoute = currentStack.last()

        // If we're at the base of the current route, go back to the start route stack.
        if (currentRoute == state.topLevelRoute){
            state.topLevelRoute = state.startRoute
        } else {
            currentStack.removeLastOrNull()
        }
    }
}

کلاس Navigator دو متد رویداد ناوبری ارائه می‌دهد:

  • به یک مسیر خاص navigate .
  • از مسیر فعلی goBack .

هر دو متد، NavigationState تغییر می‌دهند.

مرحله ۳.۳: ایجاد NavigationState و Navigator

نمونه‌هایی از NavigationState و Navigator با دامنه‌ی مشابه NavController خود ایجاد کنید.

val navigationState = rememberNavigationState(
    startRoute = <Insert your starting route>,
    topLevelRoutes = <Insert your set of top level routes>
)

val navigator = remember { Navigator(navigationState) }

مرحله ۴: جایگزینی NavController

متدهای رویداد ناوبری NavController را با معادل‌های Navigator جایگزین کنید.

فیلد یا متد NavController

معادل Navigator

navigate()

navigate()

popBackStack()

goBack()

فیلدهای NavController را با فیلدهای NavigationState جایگزین کنید.

فیلد یا متد NavController

معادل NavigationState

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

دریافت مسیر سطح بالا: برای یافتن آن، از ورودی فعلی پشته به سمت بالا در سلسله مراتب پیمایش کنید.

topLevelRoute

از NavigationState.topLevelRoute برای تعیین آیتمی که در حال حاضر در نوار ناوبری انتخاب شده است، استفاده کنید.

قبل از:

val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class)

fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
    this?.hierarchy?.any {
        it.hasRoute(route)
    } ?: false

بعد از:

val isSelected = key == navigationState.topLevelRoute

تأیید کنید که تمام ارجاعات به NavController ، از جمله هرگونه import، را حذف کرده‌اید.

مرحله ۵: مقاصد خود را از NavGraph مربوط به NavHost به یک entryProvider منتقل کنید

در ناوبری ۲، شما مقاصد خود را با استفاده از NavGraphBuilder DSL ، معمولاً درون لامبدا انتهایی NavHost ، تعریف می‌کنید. استفاده از توابع افزونه در اینجا، همانطور که در بخش «کپسوله‌سازی کد ناوبری» توضیح داده شده است، رایج است.

در ناوبری ۳، شما مقاصد خود را با استفاده از یک entryProvider تعریف می‌کنید. این entryProvider مسیری را به یک NavEntry تعیین می‌کند. نکته مهم این است که entryProvider روابط والد-فرزندی بین ورودی‌ها را تعریف نمی‌کند.

در این راهنمای مهاجرت، روابط والد-فرزند به صورت زیر مدل‌سازی شده است:

  • NavigationState مجموعه‌ای از مسیرهای سطح بالا (مسیرهای والد) و یک پشته برای هر کدام دارد. این مسیر، مسیر سطح بالای فعلی و پشته مرتبط با آن را پیگیری می‌کند.
  • هنگام پیمایش به یک مسیر جدید، Navigator بررسی می‌کند که آیا مسیر، یک مسیر سطح بالا (top-level) است یا خیر. اگر چنین باشد، مسیر سطح بالای فعلی و پشته (stack) به‌روزرسانی می‌شوند. اگر نباشد، یک مسیر فرزند (child route) است و به پشته فعلی اضافه می‌شود.

مرحله ۵.۱: ایجاد یک entryProvider

با استفاده از DSL و در همان محدوده‌ی NavigationState ، یک entryProvider ایجاد کنید.

val entryProvider = entryProvider {

}

مرحله ۵.۲: انتقال مقصدها به entryProvider

برای هر مقصد تعریف شده در NavHost ، بر اساس نوع مقصد، موارد زیر را انجام دهید:

  • navigation : آن را به همراه مسیر حذف کنید. نیازی به «مسیرهای پایه» نیست زیرا مسیرهای سطح بالا می‌توانند هر پشته پشتی تو در تو را شناسایی کنند.
  • composable<T> : آن را به entryProvider منتقل کرده و نام آن را به entry تغییر دهید، با حفظ پارامتر نوع. برای مثال، composable<RouteA> به entry<RouteA> تبدیل می‌شود.
  • dialog<T> : همان کار composable را انجام دهید، اما ابرداده را به صورت زیر به ورودی اضافه کنید: entry<T>(metadata = DialogSceneStrategy.dialog()) .
  • bottomSheet : دستورالعمل برگه پایینی را اینجا دنبال کنید . این مشابه دستورالعمل‌های dialog است، با این تفاوت که BottomSheetSceneStrategy بخشی از کتابخانه اصلی Navigation 3 نیست، بنابراین باید آن را در پروژه خود کپی کنید.

عامل هوش مصنوعی : هنگام حذف مسیرهایی که برای شناسایی یک گراف تودرتو استفاده شده‌اند، هرگونه ارجاع به مسیر حذف شده را با نوعی که برای شناسایی اولین فرزند در گراف تودرتو استفاده شده است، جایگزین کنید. برای مثال، اگر کد اصلی navigation<BaseRouteA>{ composable<RouteA>{ ... } } باشد، باید BaseRouteA حذف کرده و هرگونه ارجاع به آن را با RouteA جایگزین کنید. این جایگزینی معمولاً باید برای لیستی که به یک نوار ناوبری، ریل یا کشو ارائه می‌شود، انجام شود.

شما می‌توانید توابع الحاقی NavGraphBuilder را به توابع الحاقی EntryProviderScope<T> بازسازی کنید و سپس آنها را منتقل کنید.

آرگومان‌های ناوبری را با استفاده از کلید ارائه شده به لامبدا انتهایی entry دریافت کنید.

برای مثال:

import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.dialog
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import androidx.navigation.toRoute

@Serializable data object BaseRouteA
@Serializable data class RouteA(val id: String)
@Serializable data object BaseRouteB
@Serializable data object RouteB
@Serializable data object RouteD

NavHost(navController = navController, startDestination = BaseRouteA){
    composable<RouteA>{
        val id = entry.toRoute<RouteA>().id
        ScreenA(title = "Screen has ID: $id")
    }
    featureBSection()
    dialog<RouteD>{ ScreenD() }
}

fun NavGraphBuilder.featureBSection() {
    navigation<BaseRouteB>(startDestination = RouteB) {
        composable<RouteB> { ScreenB() }
    }
}

می‌شود:

import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.scene.DialogSceneStrategy

@Serializable data class RouteA(val id: String) : NavKey
@Serializable data object RouteB : NavKey
@Serializable data object RouteD : NavKey

val entryProvider = entryProvider {
    entry<RouteA>{ key -> ScreenA(title = "Screen has ID: ${key.id}") }
    featureBSection()
    entry<RouteD>(metadata = DialogSceneStrategy.dialog()){ ScreenD() }
}

fun EntryProviderScope<NavKey>.featureBSection() {
    entry<RouteB> { ScreenB() }
}

مرحله ۶: NavHost با NavDisplay جایگزین کنید

NavHost با NavDisplay جایگزین کنید.

  • NavHost حذف کرده و آن را با NavDisplay جایگزین کنید.
  • پارامتر entries = navigationState.toEntries(entryProvider) را مشخص کنید. این تابع، وضعیت ناوبری را به ورودی‌هایی که NavDisplay با استفاده از entryProvider نشان می‌دهد، تبدیل می‌کند.
  • NavDisplay.onBack به navigator.goBack() متصل کنید. این باعث می‌شود navigator وضعیت ناوبری را هنگام تکمیل back handler داخلی NavDisplay به‌روزرسانی کند.
  • اگر مقاصد دیالوگ دارید، DialogSceneStrategy به پارامتر sceneStrategy از NavDisplay اضافه کنید.

برای مثال:

import androidx.navigation3.ui.NavDisplay

NavDisplay(
    entries = navigationState.toEntries(entryProvider),
    onBack = { navigator.goBack() },
    sceneStrategy = remember { DialogSceneStrategy() }
)

مرحله ۷: وابستگی‌های Navigation 2 را حذف کنید

تمام ایمپورت‌های Navigation 2 و وابستگی‌های کتابخانه‌ای را حذف کنید.

خلاصه

تبریک! پروژه شما اکنون به ناوبری ۳ منتقل شده است. اگر شما یا عامل هوش مصنوعی شما در استفاده از این راهنما با مشکلی مواجه شده‌اید، اشکال خود را اینجا ثبت کنید .