Duyarlı kullanıcı arayüzleri için gezinme

Gezinme, bir uygulamanın içerik hedeflerine erişmek için kullanıcı arayüzü ile etkileşimde bulunma işlemidir. Android'in gezinme ilkeleri, tutarlı ve sezgisel bir gezinme deneyimi oluşturmanıza yardımcı olan yönergeler sunar.

Duyarlı kullanıcı arayüzleri, duyarlı içerik hedefleri sağlar ve genellikle görüntü boyutu değişikliklerine yanıt olarak farklı gezinme öğeleri içerir. Örneğin, küçük ekranlarda alt gezinme çubuğu, orta boyutlu ekranlarda gezinme çubuğu veya büyük ekranlarda kalıcı gezinme çekmecesi, duyarlı kullanıcı arayüzleri gezinme ilkelerine uymaya devam etmelidir.

Jetpack Gezinme bileşeni, gezinme ilkelerini uygular ve duyarlı kullanıcı arayüzleriyle uygulama geliştirmeyi kolaylaştırmak için kullanılabilir.

Şekil 1. Gezinme çekmecesi, çubuk ve alt çubuk bulunan genişletilmiş, orta ve kompakt ekranlar.

Duyarlı kullanıcı arayüzünde gezinme

Bir uygulamanın kapladığı ekran penceresinin boyutu ergonomiyi ve kullanılabilirliği etkiler. Pencere boyutu sınıfları, uygun gezinme öğelerini (gezinme çubukları, korkuluklar veya çekmeceler gibi) belirlemenize ve bunları kullanıcının en kolay erişebileceği yerlere yerleştirmenize olanak tanır. Materyal Tasarım düzen yönergelerinde, gezinme öğeleri ekranın ön kenarında kalıcı bir alanı kaplar ve uygulamanın genişliği kompakt olduğunda alt kenara hareket edebilir. Gezinme öğeleri seçiminiz, büyük ölçüde uygulama penceresinin boyutuna ve öğenin içermesi gereken öğe sayısına bağlıdır.

Pencere boyutu sınıfı Birkaç öğe Birçok öğe
küçük genişlik alt gezinme çubuğu gezinme çekmecesi (öndeki kenar veya alt kısım)
orta genişlik navigasyon Rayı gezinme çekmecesi (ön kenar)
genişletilmiş genişlik navigasyon Rayı kalıcı gezinme çekmecesi (ön kenar)

Görünüme dayalı düzenlerde, düzen kaynak dosyaları farklı görüntüleme boyutları için farklı gezinme öğeleri kullanmak üzere pencere boyutu sınıfı ayrılma noktalarına göre nitelenebilir. Jetpack Compose, uygulama penceresine en uygun gezinme öğesini programatik olarak belirlemek için pencere boyut sınıfı API'si tarafından sağlanan ayrılma noktalarını kullanabilir.

Görüntüleme

<!-- 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>

Oluştur

// 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
        }
    )
}

Duyarlı içerik hedefleri

Duyarlı kullanıcı arayüzünde her içerik hedefinin düzeni, pencere boyutundaki değişikliklere uyum sağlamalıdır. Uygulamanız düzen aralığını ayarlayabilir, öğeleri yeniden konumlandırabilir, içerik ekleyip kaldırabilir veya gezinme öğeleri dahil olmak üzere kullanıcı arayüzü öğelerini değiştirebilir. (Kullanıcı arayüzünüzü duyarlı düzenlere taşıma ve Farklı ekran boyutlarını destekleme konularına bakın.)

Her bir hedef yeniden boyutlandırma etkinliklerini sorunsuz şekilde işlediğinde, değişiklikler kullanıcı arayüzünde izole edilir. Navigasyon da dahil olmak üzere uygulama durumunun geri kalanı bu durumdan etkilenmez.

Gezinme, pencere boyutu değişikliklerinin yan etkisi olarak gerçekleşmemelidir. Yalnızca farklı pencere boyutlarına uyum sağlamak için içerik hedefleri oluşturmayın. Örneğin, katlanabilir cihazların farklı ekranları için farklı içerik hedefleri oluşturmayın.

Pencere boyutu değişikliklerinin yan etkisi olarak gezinmenin şu sorunları vardır:

  • Eski hedef (önceki pencere boyutu için), yeni hedefe gidilmeden önce kısa bir süre için görülebilir
  • Esnekliği korumak için (örneğin, cihaz katlanmış ve açılmışken) her pencere boyutunda gezinme gereklidir
  • Hedefler arasında uygulama durumunu korumak zor olabilir, çünkü gezinmek geri yığın patladığında durumu mahvetebilir

Ayrıca, pencere boyutu değişiklikleri gerçekleşirken uygulamanız ön planda bile olmayabilir. Uygulamanızın düzeni, ön plandaki uygulamadan daha fazla alan gerektirebilir. Kullanıcı uygulamanıza geri döndüğünde yön ve pencere boyutu değişebilir.

Uygulamanız pencere boyutuna göre benzersiz içerik hedefleri gerektiriyorsa ilgili hedefleri alternatif düzenler içeren tek bir hedefte birleştirmeyi düşünün.

Alternatif düzenlere sahip içerik hedefleri

Duyarlı tasarımın parçası olarak, tek bir gezinme hedefi, uygulama pencere boyutuna bağlı olarak alternatif düzenlere sahip olabilir. Her düzen, pencerenin tamamını kaplar ancak farklı pencere boyutları için farklı düzenler sunulur.

list-detail görünümü standart bir örnektir. Küçük pencere boyutlarında, uygulamanız liste için bir içerik düzeni, ayrıntılar için de başka bir içerik düzeni gösterir. Liste ayrıntıları görünümü hedefine gidildiğinde başlangıçta yalnızca liste düzeni görüntülenir. Bir liste öğesi seçildiğinde uygulamanız, listenin yerini alarak ayrıntı düzenini gösterir. Geri kontrolü seçildiğinde ayrıntının yerini alacak şekilde liste düzeni gösterilir. Ancak, genişletilmiş pencere boyutlarında liste ve ayrıntı düzenleri yan yana görüntülenir.

Görüntüleme

SlidingPaneLayout, büyük ekranlarda iki içerik bölmesini yan yana gösteren ancak telefon gibi küçük ekranlı cihazlarda aynı anda yalnızca bir bölme görüntüleyen tek bir gezinme hedefi oluşturmanıza olanak tanır.

<!-- 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 kullanarak liste ayrıntısı düzenini uygulamaya ilişkin ayrıntılar için İki bölmeli düzen oluşturma konusuna bakın.

Oluştur

Compose'da alternatif composable'lar, pencere boyutu sınıflarının kullanıldığı tek bir rotada birleştirilerek liste ayrıntıları görünümü uygulanabilir. Bu görünüm, her boyut sınıfı için uygun composable'ı oluşturur.

Rota, içerik hedefine giden gezinme yoludur. Bu genellikle tek bir composable'dır ancak alternatif composable'lar da olabilir. İş mantığı, alternatif composable'lardan hangilerinin gösterileceğini belirler. composable, hangi alternatifin görüntülendiğine bakılmaksızın uygulama penceresini doldurur.

Liste ayrıntısı görünümü üç composable'dan oluşur. Örneğin:

/* 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)
  }
}

Tek bir gezinme rotası, liste ayrıntılı görünümüne erişim sağlar:

@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 (gezinme hedefi), liste öğesinin seçili olup olmamasına bağlı olarak, üç composable'dan hangisinin yayınlanacağını belirler: genişletilmiş pencere boyutu için ListAndDetail; kompakt için ListOfItems veya ItemDetail.

Rota bir NavHost içindedir. Örneğin:

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

isExpandedWindowSize bağımsız değişkenini, uygulamanızın WindowMetrics'ini inceleyerek sağlayabilirsiniz.

selectedItemId bağımsız değişkeni, tüm pencere boyutlarında durumu koruyan bir ViewModel tarafından sağlanabilir. Kullanıcı listeden bir öğe seçtiğinde, selectedItemId durum değişkeni güncellenir:

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,
        /*...*/
      )
    }
  }
}

Öğe ayrıntıları "composable", uygulama penceresinin tamamını kapladığında rota, özel bir BackHandler da içerir:

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,
        /*...*/
      )
    }
  }
}

Bir ViewModel öğesindeki uygulama durumunu pencere boyutu sınıfı bilgileriyle birleştirmek, uygun composable'ı seçmeyi basit bir mantık meselesi haline getirir. Tek yönlü veri akışını sürdürürseniz uygulamanız, uygulama durumunu korurken mevcut görüntüleme alanını tam olarak kullanabilir.

Compose'da eksiksiz liste ayrıntıları görünümü uygulaması için GitHub'daki JetNews örneğine bakın.

Bir gezinme grafiği

Tüm cihazlarda veya pencere boyutlarında tutarlı bir kullanıcı deneyimi sunmak için her içerik hedefinin düzeninin uyumlu olduğu tek bir gezinme grafiği kullanın.

Her pencere boyutu sınıfı için farklı gezinme grafiği kullanırsanız uygulama bir boyut sınıfından diğerine geçtiğinde, diğer grafiklerde kullanıcının mevcut hedefini belirlemeniz, bir arka yığın oluşturmanız ve grafikler arasında farklılık gösteren durum bilgilerini uzlaştırmanız gerekir.

İç içe gezinme ana makinesi

Uygulamanız, kendine ait içerik hedefleri olan bir içerik hedefi içerebilir. Örneğin, liste ayrıntısı görünümünde öğe ayrıntısı bölmesi, öğe ayrıntısının yerini alan içeriğe giden kullanıcı arayüzü öğeleri içerebilir.

Bu tür alt gezinmeyi uygulamak için ayrıntı bölmesi, ayrıntı bölmesinden erişilen hedefleri belirten kendi gezinme grafiğine sahip olan iç içe geçmiş bir gezinme ana makinesi olabilir:

Görüntüleme

<!-- 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>

Oluştur

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

Bu, iç içe yerleştirilmiş gezinme grafiğinden farklıdır. Çünkü iç içe yerleştirilmiş NavHost gezinme grafiği, ana gezinme grafiğine bağlı değildir. Yani, bir grafikteki hedeflerden diğerindeki hedeflere doğrudan gidemezsiniz.

Daha fazla bilgi için İç içe yerleştirilmiş gezinme grafikleri ve Oluşturma ile gezinme konularına bakın.

Korunmuş durum

Duyarlı içerik hedefleri sağlamak için uygulamanızın cihaz döndürüldüğünde veya katlandığında ya da uygulama penceresi yeniden boyutlandırıldığında mevcut durumunu koruması gerekir. Varsayılan olarak, bu gibi yapılandırma değişiklikleri uygulamanın etkinliklerini, parçalarını, görünüm hiyerarşisini ve composable'larını yeniden oluşturur. Kullanıcı arayüzü durumunu kaydetmenin önerilen yolu, yapılandırma değişikliklerine rağmen devam eden bir ViewModel veya rememberSaveable kullanmaktır. (Kullanıcı arayüzü durumlarını kaydetme ve State ve Jetpack Compose konularına bakın.)

Boyut değişiklikleri tersine çevrilebilir olmalıdır (örneğin, kullanıcı cihazı döndürüp tekrar döndürdüğünde).

Duyarlı düzenler, farklı pencere boyutlarında farklı içerik parçaları görüntüleyebilir. Bu nedenle, duyarlı düzenlerin, durum geçerli pencere boyutu için geçerli olmasa bile genellikle içerikle ilgili ek durumu kaydetmesi gerekir. Örneğin, bir düzende, ek kaydırma widget'ını yalnızca daha geniş pencere genişliklerinde göstermek için alan bulunabilir. Bir yeniden boyutlandırma etkinliği pencere genişliğinin çok küçük olmasına neden oluyorsa widget gizlenir. Uygulama önceki boyutlarına göre yeniden boyutlandırıldığında, kaydırma widget'ı tekrar görünür hale gelir ve orijinal kaydırma konumunun geri yüklenmesi gerekir.

ViewModel kapsamları

Gezinme bileşenine taşıma geliştirici kılavuzu, hedeflerin parça olarak, veri modellerinin ise ViewModel kullanılarak uygulandığı tek etkinliklik bir mimari önerir.

ViewModel her zaman bir yaşam döngüsüne dahildir ve bu yaşam döngüsü kalıcı olarak sona erdiğinde ViewModel temizlenir ve silinebilir. ViewModel öğesinin kapsamının bulunduğu yaşam döngüsü (ve dolayısıyla ViewModel öğesinin ne kadar geniş bir şekilde paylaşılabileceği), ViewModel öğesini edinmek için hangi mülk yetkisinin kullanıldığına bağlıdır.

En basit şekilde, her gezinme hedefi tamamen izole bir kullanıcı arayüzü durumuna sahip tek bir parçadır. Bu nedenle, her parça viewModels() özelliği yetki belgesini kullanarak söz konusu parçaya ayarlanmış bir ViewModel elde edebilir.

Kullanıcı arayüzü durumunu parçalar arasında paylaşmak için, parçalarda activityViewModels() yöntemini çağırarak ViewModel kapsamını etkinlik kapsamında gerçekleştirin (etkinliğin eşdeğeri yalnızca viewModels()'dir). Bu, etkinliğin ve ona eklenen parçaların ViewModel örneğini paylaşmasına izin verir. Bununla birlikte, tek etkinlikli bir mimaride bu ViewModel kapsamı, uygulama süresince etkili bir şekilde kullanılabilir. Böylece ViewModel, herhangi bir parça tarafından kullanılmasa bile bellekte kalır.

Gezinme grafiğinizde bir ödeme akışını temsil eden parça hedefleri dizisi olduğunu ve tüm ödeme deneyiminin mevcut durumunun, parçalar arasında paylaşılan bir ViewModel içinde olduğunu varsayalım. ViewModel öğesinin etkinlik için kapsama alınması çok geniş kapsamlı olmaz ancak gerçekte başka bir sorunu ortaya çıkarır: Kullanıcı, bir sipariş için ödeme akışından geçer ve ardından ikinci bir sipariş için bu akıştan tekrar geçerse her iki sipariş de aynı ödeme ViewModel örneğini kullanır. İkinci sipariş ödeme işleminden önce, ilk siparişteki verileri manuel olarak temizlemeniz gerekir ve hatalar kullanıcı için maliyetli olabilir.

Bunun yerine, ViewModel kapsamını mevcut NavController içindeki bir gezinme grafiğine ayarlayın. Ödeme akışının parçası olan hedefleri içerecek şekilde iç içe yerleştirilmiş bir gezinme grafiği oluşturun. Ardından, bu parça hedeflerinin her birinde navGraphViewModels() mülk yetkisini kullanın ve paylaşılan ViewModel öğesini elde etmek için gezinme grafiğinin kimliğini iletin. Bu sayede, kullanıcı ödeme akışından çıktığında ve iç içe yerleştirilmiş gezinme grafiği kapsam dışında olduğunda, ilgili ViewModel örneği silinir ve bir sonraki ödeme işleminde kullanılmaz.

Kapsam Mülk delegesi ViewModel şunlarla paylaşılabilir:
Parça Fragment.viewModels() Yalnızca geçerli parça
Etkinlik Activity.viewModels()

Fragment.activityViewModels()

Etkinlik ve ona bağlı tüm parçalar
Gezinme grafiği Fragment.navGraphViewModels() Aynı gezinme grafiğindeki tüm parçalar

İç içe yerleştirilmiş bir gezinme ana makinesi (yukarıya bakın) kullanıyorsanız, grafikler bağlı olmadığından bu ana makinedeki hedeflerin, navGraphViewModels() kullanılırken ana makine dışındaki hedeflerle ViewModel'leri paylaşamayacağını unutmayın. Bu durumda, bunun yerine etkinliğin kapsamını kullanabilirsiniz.

Kaldırılmış durumu

Compose'da eyalet yükseltme özelliğini kullanarak pencere boyutu değişiklikleri sırasında eyaleti koruyabilirsiniz. composable'ların durumu, beste ağacında daha yüksek bir konuma yükseltildiğinde, composable'lar artık görünür olmadığında bile durum korunabilir.

Yukarıdaki Alternatif düzenlerle içerik hedefleri'nin Oluşturma bölümünde, liste ayrıntılı görünümü composable'larının durumunu ListDetailRoute değerine kaldırdık. Böylece, hangi composable'ın görüntülendiğinden bağımsız olarak durum korunacak:

@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?,
) { /*...*/ }

Ek kaynaklar