التنقّل في واجهات المستخدم المتجاوبة

التنقل هو عملية التفاعل مع واجهة مستخدم التطبيق للوصول إلى وجهات محتوى التطبيق. توفّر مبادئ التنقّل في Android إرشادات تساعدك في إنشاء تنقل متناسق وبديهي في التطبيقات.

توفّر واجهات المستخدم المتجاوبة وجهات محتوى متجاوبة وغالبًا ما تتضمّن أنواعًا مختلفة من عناصر التنقّل استجابةً لتغييرات حجم العرض، على سبيل المثال، شريط تنقّل سفلي على الشاشات الصغيرة أو شريط تنقُّل على الشاشات متوسطة الحجم أو درج تنقُّل دائم على الشاشات الكبيرة، ولكن يجب أن تتوافق واجهات المستخدم سريعة الاستجابة مع مبادئ التنقّل.

ينفِّذ مكوِّن التنقّل في Jetpack مبادئ التنقّل ويمكن استخدامه لتسهيل تطوير التطبيقات ذات واجهات المستخدم المتجاوبة.

الشكل 1. شاشات موسَّعة ومتوسطة وصغيرة تضم درجًا للتنقل وشريطًا وشريطًا سفليًا

التنقّل السريع في واجهة المستخدم

يؤثّر حجم نافذة العرض التي يشغلها أحد التطبيقات في سهولة الاستخدام وسهولة الاستخدام. تتيح لك فئات حجم النوافذ تحديد عناصر التنقّل المناسبة (مثل أشرطة التنقّل أو القضبان أو الأدراج) ووضعها في الأماكن التي يسهل على المستخدِم الوصول إليها فيها. في إرشادات التصميم المتعلقة بالتصميم المتعدد الأبعاد، تشغل عناصر التنقل مساحة دائمة على الحافة الرئيسية للشاشة، ويمكن أن تنتقل إلى الحافة السفلية عندما يكون عرض التطبيق مضغوطًا. يعتمد اختيارك لعناصر التنقل إلى حد كبير على حجم نافذة التطبيق وعدد العناصر التي يجب أن يحتوي عليها العنصر.

فئة حجم النافذة عناصر قليلة العديد من العناصر
عرض مضغوط شريط التنقّل السفلي لائحة التنقل (الحافة الأمامية أو السفلية)
عرض متوسط شريط التنقّل لائحة التنقل (الحافة الرئيسية)
عرض موسّع شريط التنقّل لائحة التنقل المستمر (الحافة الرئيسية)

في التنسيقات المستندة إلى العرض، يمكن تأهيل ملفات موارد التصميم حسب نقاط الفواصل الإعلانية لفئة حجم النافذة لاستخدام عناصر تنقل مختلفة لأبعاد العرض المختلفة. يمكن أن يستخدم Jetpack Compose نقاط التوقف التي توفّرها واجهة برمجة تطبيقات فئة حجم النافذة لتحديد عنصر التنقّل الأكثر ملاءمةً لنافذة التطبيق آليًا.

عدد المشاهدات

<!-- res/layout/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>


<!-- res/layout-w600dp/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.navigationrail.NavigationRailView
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>


<!-- res/layout-w1240dp/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.navigation.NavigationView
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

إنشاء

// This method should be run inside a Composable function.
val widthSizeClass = calculateWindowSizeClass(this).widthSizeClass
// You can get the height of the current window by invoking heightSizeClass instead.

@Composable
fun MyApp(widthSizeClass: WindowWidthSizeClass) {
    // Select a navigation element based on window size.
    when (widthSizeClass) {
        WindowWidthSizeClass.Compact -> { CompactScreen() }
        WindowWidthSizeClass.Medium -> { MediumScreen() }
        WindowWidthSizeClass.Expanded -> { ExpandedScreen() }
    }
}

@Composable
fun CompactScreen() {
    Scaffold(bottomBar = {
                NavigationBar {
                    icons.forEach { item ->
                        NavigationBarItem(
                            selected = isSelected,
                            onClick = { ... },
                            icon = { ... })
                    }
                }
            }
        ) {
        // Other content
    }
}

@Composable
fun MediumScreen() {
    Row(modifier = Modifier.fillMaxSize()) {
        NavigationRail {
            icons.forEach { item ->
                NavigationRailItem(
                    selected = isSelected,
                    onClick = { ... },
                    icon = { ... })
            }
        }
        // Other content
    }
}

@Composable
fun ExpandedScreen() {
    PermanentNavigationDrawer(
        drawerContent = {
            icons.forEach { item ->
                NavigationDrawerItem(
                    icon = { ... },
                    label = { ... },
                    selected = isSelected,
                    onClick = { ... }
                )
            }
        },
        content = {
            // Other content
        }
    )
}

وجهات المحتوى المتجاوب

في واجهة المستخدم المتجاوبة، يجب أن يتكيف تصميم كل وجهة محتوى مع التغييرات التي تطرأ على حجم النافذة. يمكن لتطبيقك ضبط تباعد التصميم أو تغيير موضع العناصر أو إضافة محتوى أو إزالته أو تغيير عناصر واجهة المستخدم، بما في ذلك عناصر التنقل. (راجِع نقل واجهة المستخدم إلى التنسيقات المتجاوبة وإتاحة أحجام الشاشات المختلفة).

عندما تتعامل كل وجهة فردية مع أحداث تغيير الحجم بسلاسة، يتم عزل التغييرات على واجهة المستخدم. ولا تتأثّر بقية حالة التطبيق، بما في ذلك التنقّل.

يجب ألا يحدث التنقل من الآثار الجانبية لتغييرات حجم النافذة. لا تنشئ وجهات محتوى لتلائم أحجام النوافذ المختلفة فقط. على سبيل المثال، يجب عدم إنشاء وجهات محتوى مختلفة للشاشات المختلفة لجهاز قابل للطي.

تحدث المشاكل التالية في عملية الانتقال كأثر جانبي لتغييرات حجم النافذة:

  • قد تظهر الوجهة القديمة (لحجم النافذة السابقة) مؤقتًا قبل الانتقال إلى الوجهة الجديدة.
  • للحفاظ على إمكانية عكس الشاشة (على سبيل المثال، عند طي الجهاز وفتحه)، يجب التنقّل لكل حجم نافذة.
  • قد يكون من الصعب الحفاظ على حالة التطبيق بين الوجهات، لأنّ التنقّل يمكن أن يؤدي إلى تدمير الحالة عند فتح وضع "الحزمة الخلفية"

بالإضافة إلى ذلك، قد لا يكون تطبيقك في المقدّمة أثناء حدوث تغييرات في حجم النافذة. قد يتطلب تنسيق تطبيقك مساحة أكبر من التطبيق الذي يعمل في المقدّمة، وعندما يعود المستخدم إلى تطبيقك، من الممكن أن يتغيّر كل من الاتجاه وحجم النافذة.

إذا كان تطبيقك يتطلب وجهات محتوى فريدة استنادًا إلى حجم النافذة، ننصحك بدمج الوجهات ذات الصلة في وجهة واحدة تشتمل على تنسيقات بديلة.

وجهات المحتوى بتنسيقات بديلة

كجزء من التصميم سريع الاستجابة، يمكن أن تحتوي وجهة التنقل الفردية على تخطيطات بديلة اعتمادًا على حجم نافذة التطبيق. يشغل كل تخطيط النافذة بأكملها، ولكن يتم تقديم تخطيطات مختلفة لأحجام نوافذ مختلفة.

ومن الأمثلة الأساسية على ذلك عرض القائمة التفصيلية. بالنسبة إلى أحجام النوافذ الصغيرة، يعرض تطبيقك تنسيقًا واحدًا للمحتوى للقائمة وآخرًا للتفاصيل. يعرض الانتقال إلى وجهة عرض القائمة التفصيلية تنسيق القائمة فقط في البداية. عند تحديد عنصر قائمة، يعرض تطبيقك تنسيق التفاصيل، ويحل محل القائمة. عند تحديد عنصر التحكم الخلفي، يتم عرض تنسيق القائمة ويحل محل التفاصيل. ومع ذلك، بالنسبة إلى أحجام النوافذ الموسّعة، يتم عرض تنسيقات القائمة والتفاصيل جنبًا إلى جنب.

عدد المشاهدات

تتيح لك أداة SlidingPaneLayout إنشاء وجهة تنقّل واحدة تعرض جزءَين من المحتوى جنبًا إلى جنب على الشاشات الكبيرة، ولكن على جزء واحد فقط في كل مرة على الأجهزة ذات الشاشات الصغيرة، مثل الهواتف.

<!-- Single destination for list and detail. -->

<navigation ...>

    <!-- Fragment that implements SlidingPaneLayout. -->
    <fragment
        android:id="@+id/article_two_pane"
        android:name="com.example.app.ListDetailTwoPaneFragment" />

    <!-- Other destinations... -->
</navigation>

يُرجى الاطّلاع على إنشاء تنسيق من جزأين للحصول على تفاصيل حول تنفيذ تنسيق قائمة مفصّلة باستخدام SlidingPaneLayout.

إنشاء

في نافذة Compose، يمكن تنفيذ عرض تفاصيل القائمة من خلال دمج العناصر البديلة القابلة للإنشاء في مسار واحد يستخدم فئات حجم النافذة لإصدار المحتوى المناسب القابل للإنشاء لكل فئة من فئات الحجم.

المسار هو مسار التنقل إلى وجهة محتوى، وعادةً ما يكون مسارًا واحدًا قابلاً للإنشاء ولكن يمكن أيضًا أن يكون عناصر بديلة قابلة للإنشاء. يحدّد منطق النشاط التجاري العناصر البديلة القابلة للإنشاء التي يتم عرضها. يملأ العنصر القابل للإنشاء نافذة التطبيق بغض النظر عن البديل المعروض.

تتألف طريقة عرض القائمة التفصيلية من ثلاثة عناصر قابلة للإنشاء، على سبيل المثال:

/* Displays a list of items. */
@Composable
fun ListOfItems(
    onItemSelected: (String) -> Unit,
) { /*...*/ }

/* Displays the detail for an item. */
@Composable
fun ItemDetail(
    selectedItemId: String? = null,
) { /*...*/ }

/* Displays a list and the detail for an item side by side. */
@Composable
fun ListAndDetail(
    selectedItemId: String? = null,
    onItemSelected: (String) -> Unit,
) {
  Row {
    ListOfItems(onItemSelected = onItemSelected)
    ItemDetail(selectedItemId = selectedItemId)
  }
}

يوفّر مسار التنقّل الفردي إمكانية الوصول إلى عرض القائمة التفصيلية:

@Composable
fun ListDetailRoute(
    // Indicates that the display size is represented by the expanded window size class.
    isExpandedWindowSize: Boolean = false,
    // Identifies the item selected from the list. If null, a item has not been selected.
    selectedItemId: String?,
) {
  if (isExpandedWindowSize) {
    ListAndDetail(
      selectedItemId = selectedItemId,
      /*...*/
    )
  } else {
    // If the display size cannot accommodate both the list and the item detail,
    // show one of them based on the user's focus.
    if (selectedItemId != null) {
      ItemDetail(
        selectedItemId = selectedItemId,
        /*...*/
      )
    } else {
      ListOfItems(/*...*/)
    }
  }
}

تحدِّد ListDetailRoute (وجهة التنقّل) أيًا من العناصر الثلاثة القابلة للإنشاء التي سيتم إنشاؤها: ListAndDetail لحجم النافذة الموسّعة، أو ListOfItems أو ItemDetail للنافذة المكثفة، استنادًا إلى ما إذا تم اختيار عنصر قائمة.

تم تضمين المسار في NavHost، على سبيل المثال:

NavHost(navController = navController, startDestination = "listDetailRoute") {
  composable("listDetailRoute") {
    ListDetailRoute(isExpandedWindowSize = isExpandedWindowSize,
                    selectedItemId = selectedItemId)
  }
  /*...*/
}

يمكنك توفير الوسيطة isExpandedWindowSize من خلال فحص WindowMetrics في تطبيقك.

يمكن توفير الوسيطة selectedItemId من خلال ViewModel، وهي تحافظ على حالتها في جميع أحجام النوافذ. عندما يختار المستخدم عنصرًا من القائمة، يتم تعديل متغيّر الحالة selectedItemId:

class ListDetailViewModel : ViewModel() {

  data class ListDetailUiState(
      val selectedItemId: String? = null,
  )

  private val viewModelState = MutableStateFlow(ListDetailUiState())

  fun onItemSelected(itemId: String) {
    viewModelState.update {
      it.copy(selectedItemId = itemId)
    }
  }
}

val listDetailViewModel = ListDetailViewModel()

@Composable
fun ListDetailRoute(
    isExpandedWindowSize: Boolean = false,
    selectedItemId: String?,
    onItemSelected: (String) -> Unit = { listDetailViewModel.onItemSelected(it) },
) {
  if (isExpandedWindowSize) {
    ListAndDetail(
      selectedItemId = selectedItemId,
      onItemSelected = onItemSelected,
      /*...*/
    )
  } else {
    if (selectedItemId != null) {
      ItemDetail(
        selectedItemId = selectedItemId,
        /*...*/
      )
    } else {
      ListOfItems(
        onItemSelected = onItemSelected,
        /*...*/
      )
    }
  }
}

يتضمّن المسار أيضًا BackHandler مخصّصًا عندما تشغل تفاصيل العنصر القابل للإنشاء نافذة التطبيق بالكامل:

class ListDetailViewModel : ViewModel() {

  data class ListDetailUiState(
      val selectedItemId: String? = null,
  )

  private val viewModelState = MutableStateFlow(ListDetailUiState())

  fun onItemSelected(itemId: String) {
    viewModelState.update {
      it.copy(selectedItemId = itemId)
    }
  }

  fun onItemBackPress() {
    viewModelState.update {
      it.copy(selectedItemId = null)
    }
  }
}

val listDetailViewModel = ListDetailViewModel()

@Composable
fun ListDetailRoute(
    isExpandedWindowSize: Boolean = false,
    selectedItemId: String?,
    onItemSelected: (String) -> Unit = { listDetailViewModel.onItemSelected(it) },
    onItemBackPress: () -> Unit = { listDetailViewModel.onItemBackPress() },
) {
  if (isExpandedWindowSize) {
    ListAndDetail(
      selectedItemId = selectedItemId,
      onItemSelected = onItemSelected,
      /*...*/
    )
  } else {
    if (selectedItemId != null) {
      ItemDetail(
        selectedItemId = selectedItemId,
        /*...*/
      )
      BackHandler {
        onItemBackPress()
      }
    } else {
      ListOfItems(
        onItemSelected = onItemSelected,
        /*...*/
      )
    }
  }
}

عند الجمع بين حالة التطبيق من ViewModel ومعلومات فئة حجم النافذة، يصبح اختيار الحالة القابلة للإنشاء المناسبة أمرًا منطقيًا. من خلال الحفاظ على تدفق البيانات أحادي الاتجاه، يمكن لتطبيقك استخدام مساحة العرض المتاحة بالكامل مع الحفاظ على حالة التطبيق.

للحصول على تنفيذ كامل لعرض القائمة التفصيلية في Compose، يمكنك الاطّلاع على نموذج JetNews على GitHub.

رسم بياني واحد للتنقّل

لتوفير تجربة مستخدم متسقة على أي حجم جهاز أو نافذة، استخدِم رسمًا بيانيًا واحدًا للتنقّل حيث يكون تصميم كل وجهة محتوى متجاوبًا.

إذا كنت تستخدم رسمًا بيانيًا مختلفًا للتنقل لكل فئة من فئات حجم النافذة، عندما ينتقل التطبيق من فئة حجم إلى أخرى، عليك تحديد الوجهة الحالية للمستخدم في الرسومات البيانية الأخرى، وإنشاء حزمة خلفية وتوفيق معلومات الحالة التي تختلف بين الرسومات البيانية.

مضيف التنقل المُدمج

قد يتضمن تطبيقك وجهة محتوى لها وجهات خاصة بها للمحتوى. على سبيل المثال، في عرض القائمة التفصيلية، يمكن أن يتضمن جزء تفاصيل العنصر عناصر واجهة المستخدم التي تنتقل إلى المحتوى الذي يحل محل تفاصيل العنصر.

لتنفيذ هذا النوع من التنقّل الفرعي، يمكن أن يكون جزء التفاصيل مضيف تنقُّل متداخل مع رسم بياني للتنقّل الفرعي يحدد الوجهات التي يتم الوصول إليها من جزء التفاصيل:

عدد المشاهدات

<!-- layout/two_pane_fragment.xml -->

<androidx.slidingpanelayout.widget.SlidingPaneLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/sliding_pane_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list_pane"
        android:layout_width="280dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"/>

    <!-- Detail pane is a nested navigation host. Its graph is not connected
         to the main graph that contains the two_pane_fragment destination. -->
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/detail_pane"
        android:layout_width="300dp"
        android:layout_weight="1"
        android:layout_height="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/detail_pane_nav_graph" />
</androidx.slidingpanelayout.widget.SlidingPaneLayout>

إنشاء

@Composable
fun ItemDetail(selectedItemId: String? = null) {
    val navController = rememberNavController()
    NavHost(navController, "itemSubdetail1") {
        composable("itemSubdetail1") { ItemSubdetail1(...) }
        composable("itemSubdetail2") { ItemSubdetail2(...) }
        composable("itemSubdetail3") { ItemSubdetail3(...) }
    }
}

ويختلف هذا عن الرسم البياني المتداخل للتنقّل لأنّ الرسم البياني الخاص بالتنقل الخاص بعنصر NavHost المدمَج غير مرتبط بالرسم البياني الرئيسي للتنقّل، وهذا يعني أنّه لا يمكنك الانتقال مباشرةً من الوجهات في رسم بياني إلى وجهات في الأخرى.

للحصول على مزيد من المعلومات، اطلع على الرسوم البيانية المتداخلة للتنقل والتنقل باستخدام ميزة الإنشاء.

حالة محفوظة

لتوفير وجهات محتوى متجاوب، يجب أن يحافظ تطبيقك على حالته عند تدوير الجهاز أو طيه أو عند تغيير حجم نافذة التطبيق. بشكلٍ تلقائي، تعمل تغييرات الإعدادات، مثل هذه على إعادة إنشاء أنشطة التطبيق وأجزاءه وعرض التدرّج الهرمي والعناصر القابلة للإنشاء. والطريقة المقترحة لحفظ حالة واجهة المستخدم هي استخدام ViewModel أو rememberSaveable، علمًا بأنّهما يستمران في جميع تغييرات الإعدادات. (راجِع حفظ حالات واجهة المستخدم والحالة وJetpack Compose.)

يجب أن تكون التغييرات في الحجم قابلة للعكس، على سبيل المثال، عندما يقوم المستخدم بتدوير الجهاز ثم تدويره مرة أخرى.

يمكن أن تعرض التخطيطات سريعة الاستجابة أجزاءً مختلفة من المحتوى بأحجام نوافذ مختلفة؛ وبالتالي، تحتاج التخطيطات سريعة الاستجابة غالبًا إلى توفير حالة إضافية تتعلق بالمحتوى، حتى إذا كانت الحالة لا تنطبق على حجم النافذة الحالي. على سبيل المثال، قد يحتوي التخطيط على مساحة لعرض أداة تمرير إضافية فقط بعرض نافذة أكبر. إذا تسبب حدث تغيير الحجم في جعل عرض النافذة صغيرًا جدًا، فسيتم إخفاء الأداة. عندما يتم تغيير حجم التطبيق ليتناسب مع أبعاده السابقة، تصبح أداة التمرير مرئية مرة أخرى، ويجب استعادة موضع التمرير الأصلي.

نطاقات ViewModel

يقترح دليل المطوِّرين النقل إلى مكوِّن التنقل إنشاء بنية تعمل لمرة واحدة يتم فيها تنفيذ الوجهات كأجزاء وتنفيذ نماذج البيانات باستخدام ViewModel.

تحدّد السمة ViewModel دائمًا نطاق دورة حياة معيّنة، وبعد انتهاء هذه العملية نهائيًا، يتم محو السمة ViewModel ويمكن تجاهلها. تعتمد دورة النشاط التي يتم فيها تحديد نطاق ViewModel، وبالتالي مدى إمكانية مشاركة ViewModel، على تفويض الموقع الذي يتم استخدامه للحصول على ViewModel.

في أبسط الحالات، تكون كل وجهة تنقُّل عبارة عن جزء واحد مع حالة واجهة مستخدم معزولة تمامًا، وبالتالي، يمكن لكل جزء استخدام تفويض السمة viewModels() للحصول على ViewModel على مستوى هذا الجزء.

لمشاركة حالة واجهة المستخدم بين الأجزاء، يمكنك نطاق ViewModel على النشاط من خلال استدعاء activityViewModels() في الأجزاء (ما يعادل النشاط فقط viewModels()). ما يسمح بالنشاط وأي أجزاء يتم إرفاقها به لمشاركة مثيل ViewModel. أمّا في بنية النشاط الواحد، يظل نطاق ViewModel هذا فعّالاً طالما أنّ التطبيق، لذلك يظل ViewModel في الذاكرة حتى في حال عدم استخدام أي أجزاء له.

لنفترض أنّ الرسم البياني للتنقّلي يحتوي على سلسلة من وجهات مجزّأة تمثّل مسار الدفع، والحالة الحالية لتجربة الدفع بأكملها هي في ViewModel تتم مشاركته بين الأجزاء. إنّ تحديد نطاق ViewModel على النشاط ليس واسعًا جدًا وحسب، بل يعرض في الواقع مشكلة أخرى: إذا اجتاز المستخدم مسار الدفع لأحد الطلبات، ثم واصل الطلب مرة أخرى لطلب ثانٍ، يستخدم كلا الطلبَين مثيل عملية الدفع ViewModel نفسه. قبل دفع الطلب الثاني، سيتعين عليك محو البيانات يدويًا من الطلب الأول، وقد تكون أي أخطاء مكلفة على المستخدم.

بدلاً من ذلك، يمكنك نطاق ViewModel على رسم بياني للتنقّل في NavController الحالي. أنشئ رسمًا بيانيًا للتنقل متداخلاً لتضمين الوجهات التي تشكل جزءًا من مسار الدفع. وبعد ذلك، في كل وجهة من وجهات التقسيم هذه، استخدِم تفويض السمة navGraphViewModels()، ومرِّر رقم تعريف الرسم البياني للتنقّل للحصول على ViewModel المشترك. يضمن ذلك أنّه عندما يخرج المستخدم من مسار الدفع ويكون الرسم البياني للتنقّلي المُدمَج خارج النطاق، يتم تجاهل مثيل ViewModel ولن يتم استخدامه في عملية الدفع التالية.

المجال تفويض الموقع يمكن مشاركة ViewModel مع
جزء Fragment.viewModels() الجزء الحالي فقط
النشاط Activity.viewModels()

Fragment.activityViewModels()

النشاط وجميع الأجزاء المرتبطة به
الرسم البياني للتنقل Fragment.navGraphViewModels() جميع الأجزاء في الرسم البياني نفسه للتنقل

تجدر الإشارة إلى أنّه في حال استخدام مضيف تنقّل مدمج (انظر أعلاه)، لا يمكن للوجهات في هذا المضيف مشاركة ViewModel مع وجهات خارج المضيف عند استخدام navGraphViewModels() بسبب عدم ربط الرسومات البيانية. في هذه الحالة، يمكنك استخدام نطاق النشاط بدلاً من ذلك.

حالة الرفع

في Compose، يمكنك الحفاظ على الحالة أثناء تغييرات حجم النافذة من خلال رفع الحالة. ومن خلال رفع حالة العناصر القابلة للإنشاء إلى موضع أعلى في شجرة التركيب، يمكن الحفاظ على الحالة حتى إذا لم تعُد تلك المواد مرئية.

في القسم إنشاء ضمن وجهات المحتوى ذات التنسيقات البديلة أعلاه، نقلنا حالة عرض تفاصيل القائمة القابلة للإنشاء إلى ListDetailRoute، كي يتم الاحتفاظ بهذه الحالة بصرف النظر عن العنصر القابل للإنشاء الذي يتم عرضه:

@Composable
fun ListDetailRoute(
    // Indicates that the display size is represented by the expanded window size class.
    isExpandedWindowSize: Boolean = false,
    // Identifies the item selected from the list. If null, a item has not been selected.
    selectedItemId: String?,
) { /*...*/ }

مصادر إضافية