Gezinme, bir uygulamanın içerik hedeflerine erişmek için uygulamanın kullanıcı arayüzüyle etkileşim kurma sürecidir. Android'in gezinme ilkeleri; uygulamalarda tutarlı ve sezgisel bir gezinme deneyimi oluşturmanıza yardımcı olan yönergeler sağlar.
Duyarlı kullanıcı arayüzleri, duyarlı içerik hedefleri sunar ve genellikle görüntü boyutu değişikliklerine yanıt olarak farklı türde gezinme öğeleri içerir. Örneğin, küçük ekranlarda alt gezinme çubuğu, orta boyutlu ekranlarda gezinme bölmesi veya büyük ekranlarda kalıcı bir gezinme çekmecesi. Ancak duyarlı kullanıcı arayüzleri, gezinme ilkelerine uymaya devam etmelidir.
Jetpack Gezinme bileşeni, gezinme ilkelerini uygular ve duyarlı kullanıcı arayüzlerine sahip uygulamaların geliştirilmesini kolaylaştırmak için kullanılabilir.
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ı için en erişilebilir 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 küçük olduğunda alt kenara taşınabilir. 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ı | Az sayıda öğe | Birçok öğe |
---|---|---|
en yüksek genişlik | alt gezinme çubuğu | gezinme çekmecesi (ön kenar veya alt) |
orta genişlik | gezinme çubuğu | gezinme çekmecesi (ön kenar) |
genişletilmiş genişlik | gezinme çubuğu | kalıcı gezinme çekmecesi (ön kenar) |
Görünüme dayalı düzenlerde, düzen kaynak dosyaları, farklı görüntü boyutları için farklı gezinme öğeleri kullanmak amacıyla pencere boyutu sınıfı ayrılma noktalarına göre değerlendirilebilir. Jetpack Compose, uygulama penceresine en uygun gezinme öğesini programatik olarak belirlemek için window size class API tarafından sağlanan ayrılma noktalarını kullanabilir.
Görüntüleme sayısı
<!-- 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 de dahil olmak üzere kullanıcı arayüzü öğelerini değiştirebilir. (Kullanıcı arayüzünü duyarlı düzenlere taşıma ve Farklı ekran boyutlarını destekleme konularına bakın.)
Her bir hedef yeniden boyutlandırma etkinliklerini sorunsuz bir şekilde ele aldığında değişiklikler kullanıcı arayüzünde izole edilir. Gezinme de dahil olmak üzere uygulama durumunun geri kalanı 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 gezinirken aşağıdaki sorunlar ortaya çıkar:
- Eski hedef (önceki pencere boyutu için), yeni hedefe gitmeden önce kısa bir süre içinde görünebilir
- Geri döndürülebilirliği sağlamak amacıyla (örneğin, bir cihaz katlanmış ve açılmış olduğunda) her pencere boyutunda gezinme gerekir
- Gezinme, sırt yığını patladığında durumu yok edebileceğinden hedefler arasında uygulama durumunu korumak zor olabilir.
Ayrıca, pencere boyutu değişiklikleri yapılırken uygulamanız ön planda bile olmayabilir. Uygulamanızın düzeni, ön plandaki uygulamadan daha fazla alan gerektirebilir. Ayrıca kullanıcı uygulamanıza geri döndüğünde yönü ve pencere boyutu değişmiş olabilir.
Uygulamanız için pencere boyutuna göre benzersiz içerik hedefleri gerekiyorsa alakalı hedefleri, alternatif düzenler içeren tek bir hedefte birleştirmeyi düşünün.
Alternatif düzenlerin kullanıldığı içerik hedefleri
Duyarlı tasarımın bir parçası olarak, tek bir gezinme hedefi, uygulama pencere boyutuna bağlı olarak alternatif düzenler içerebilir. Her düzen tüm pencereyi kaplar ancak farklı pencere boyutları için farklı düzenler sunulur.
Standart bir örnek, list-detail görünümüdür. Uygulamanız, küçük pencere boyutlarında liste ve ayrıntılar için birer içerik düzeni görüntüler. 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 ayrıntı düzenini görüntüler ve listenin yerini alır. Geri kontrolü seçildiğinde, ayrıntının yerini alan liste düzeni görüntülenir. Ancak genişletilmiş pencere boyutları için liste ve ayrıntı düzenleri yan yana görüntülenir.
Görüntüleme sayısı
SlidingPaneLayout
, büyük ekranlarda iki içerik bölmesini yan yana gösteren, ancak telefonlar gibi küçük ekranlı cihazlarda aynı anda yalnızca bir bölme gösteren 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ıları düzeni uygulama ile ilgili ayrıntılar için İki bölmeli düzen oluşturma bölümüne bakın.
Oluştur
Compose'da, her boyut sınıfı için uygun composable'ı yayınlamak üzere pencere boyutu sınıflarını kullanan tek bir yönlendirme, alternatif composable'ları birleştirerek bir liste ayrıntıları görünümü uygulanabilir.
Rota, genellikle tek bir composable olan, ancak alternatif composable'lar da olabilen içerik hedefine giden gezinme yoludur. İş mantığı, alternatif composable'lardan hangisinin gösterileceğini belirler. composable, görüntülenen alternatiften bağımsız olarak 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ıları 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), üç composable'dan hangisinin yayınlanacağını belirler: Genişletilmiş pencere boyutu için ListAndDetail
; Liste öğesinin seçili olup olmamasına bağlı olarak en yüksek boyut için ListOfItems
veya ItemDetail
.
Rota, NavHost
içinde yer alır. Örneğin:
NavHost(navController = navController, startDestination = "listDetailRoute") {
composable("listDetailRoute") {
ListDetailRoute(isExpandedWindowSize = isExpandedWindowSize,
selectedItemId = selectedItemId)
}
/*...*/
}
Uygulamanızın WindowMetrics'ini inceleyerek isExpandedWindowSize
bağımsız değişkenini 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,
/*...*/
)
}
}
}
Rota, composable öğe ayrıntısı uygulama penceresinin tamamını kapladığında ö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,
/*...*/
)
}
}
}
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ürerek uygulamanız, uygulama durumunu korurken mevcut görüntüleme alanını tam olarak kullanabilir.
Compose'da tam 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 sağlamak için her içerik hedefi düzeninin duyarlı olduğu tek bir gezinme grafiği kullanın.
Her pencere boyutu sınıfı için farklı bir gezinme grafiği kullanıyorsanız, uygulama bir boyut sınıfından diğerine her geçiş yaptığında, diğer grafiklerde kullanıcının o anki hedefini belirlemeniz, bir arka yığın oluşturmanız ve grafikler arasında farklılık gösteren durum bilgilerini eşleştirmeniz gerekir.
İç içe gezinme ana makinesi
Uygulamanız, kendi içerik hedefleri olan bir içerik hedefi barındırıyor olabilir. Örneğin, liste ayrıntıları 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 bir alt gezinmeyi uygulamak için ayrıntı bölmesi, ayrıntı bölmesinden erişilen hedefleri belirten kendi gezinme grafiğine sahip iç içe yerleştirilmiş bir gezinme ana makinesi olabilir:
Görüntüleme sayısı
<!-- 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
öğesine ait gezinme grafiği ana gezinme grafiğine bağlı değildir; Yani bir grafikteki hedeflerden diğerindeki hedeflere doğrudan gidemezsiniz.
Daha fazla bilgi edinmek 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 durumunu koruması gerekir. Varsayılan olarak, bunlar gibi yapılandırma değişiklikleri uygulamanın etkinliklerini, parçalarını, görünüm hiyerarşisini ve composable'ları yeniden oluşturur. Kullanıcı arayüzü durumunu kaydetmenin önerilen yolu, yapılandırma değişikliklerinden sonra da kalmaya devam eden bir ViewModel
veya rememberSaveable
kullanmaktır. (Kullanıcı arayüzü durumlarını kaydetme ve Durum ve Jetpack Compose konularına bakın.)
Boyut değişiklikleri tersine çevrilebilmelidir (örneğin, kullanıcı cihazı döndürüp tekrar döndürdüğünde).
Duyarlı düzenler, farklı içerik parçalarını farklı pencere boyutlarında gösterebilir. Bu nedenle, geçerli pencere boyutu için geçerli olmasa bile duyarlı düzenlerin genellikle içerikle ilgili ek durum kaydetmesi gerekir. Örneğin, bir düzende yalnızca daha büyük pencere genişliklerinde ek kaydırma widget'ını gösterecek bir alan olabilir. Bir yeniden boyutlandırma etkinliği pencere genişliğinin çok küçük olmasına neden olursa widget gizlenir. Uygulama önceki boyutlarına yeniden boyutlandırıldığında, kaydırma widget'ı tekrar görünür hale gelir ve orijinal kaydırma konumu geri yüklenmelidir.
ViewModel kapsamları
Gezinme bileşenine taşıma geliştirici kılavuzunda, hedeflerin parçalar olarak uygulandığı ve veri modellerinin ViewModel
kullanılarak uygulandığı tek etkinlikli bir mimari önerilir.
ViewModel
her zaman bir yaşam döngüsü kapsamındadır ve bu yaşam döngüsü kalıcı olarak sona erdiğinde ViewModel
temizlenir ve silinebilir. ViewModel
öğesinin kapsama dahil olduğu yaşam döngüsü ve dolayısıyla ViewModel
öğesinin ne kadar geniş kapsamlı olarak paylaşılabileceği, ViewModel
öğesini elde etmek için yetki verilen mülk türüne bağlıdır.
En basit şekilde ifade etmek gerekirse her gezinme hedefi, tamamen yalıtılmış bir kullanıcı arayüzü durumuna sahip tek bir parçadır; Dolayısıyla her parça, viewModels()
özelliği tarafından yetki verilen bu parçanın kapsamında bir ViewModel
elde etmek için kullanılabilir.
Parçalar arasında kullanıcı arayüzü durumunu paylaşmak için parçalarda activityViewModels()
yöntemini çağırarak ViewModel
kapsamının kapsamını belirleyin (etkinliğin eşdeğeri yalnızca viewModels()
). Bu işlem, etkinliğin ve ona eklenen tüm parçaların ViewModel
örneğini paylaşmasına izin verir. Ancak tek etkinlikli bir mimaride bu ViewModel
kapsamı, uygulama olduğu sürece etkin kalır. Böylece ViewModel
, herhangi bir parça kullanmasa bile bellekte kalır.
Gezinme grafiğinizde bir ödeme akışını temsil eden parça hedeflerden oluşan bir dizi olduğunu ve tüm ödeme deneyimi için mevcut durumun parçalar arasında paylaşılan bir ViewModel
içinde olduğunu varsayalım. Etkinliğin ViewModel
kapsamı, yalnızca çok geniş kapsamlı olmakla kalmaz, aynı zamanda başka bir sorunu da ortaya çıkarır: Kullanıcı, bir sipariş için ödeme akışından geçer ve ardından devam ederse
ikinci sipariş için tekrar sipariş etse de, her iki siparişte de aynı
ödeme ViewModel
. İkinci sipariş ödemesinden önce, ilk siparişe ait verileri manuel olarak temizlemeniz gerekir. Böyle bir durumda yapılacak herhangi bir hata kullanıcı açısından maliyetli olabilir.
Bunun yerine, ViewModel
öğesinin kapsamını mevcut NavController
içindeki bir gezinme grafiğine ayarlayın. Ödeme akışının parçası olan hedefleri özetlemek için iç içe yerleştirilmiş bir gezinme grafiği oluşturun. Ardından, bu parça hedeflerinin her birinde navGraphViewModels()
mülk yetkilendirmesini 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 kalırsa ilgili ViewModel
örneği silinir ve bir sonraki ödeme için kullanılmaz.
Kapsam | Mülke erişim yetkisi | ViewModel öğesini şu kullanıcılarla paylaşabilir: |
---|---|---|
Parça | Fragment.viewModels() |
Yalnızca geçerli parça |
Etkinlik | Activity.viewModels()
|
Etkinlik ve buna ekli tüm parçalar |
Gezinme grafiği | Fragment.navGraphViewModels() |
Aynı gezinme grafiğindeki tüm parçalar |
İç içe yerleştirilmiş gezinme ana makinesi kullanıyorsanız (yukarıya bakın), navGraphViewModels()
kullanılırken grafikler bağlı olmadığından söz konusu ana makinedeki hedefler, ana makine dışındaki hedeflerle ViewModel
öğelerini paylaşamaz. Bu durumda, bunun yerine etkinliğin kapsamını kullanabilirsiniz.
Kaldırılmış durumu
Oluşturma'da, durum kaldırma ile pencere boyutu değişiklikleri sırasında durumu koruyabilirsiniz. composable'ların durumunu kompozisyon ağacında daha yüksek bir konuma kaldırarak, composable'lar artık görünür olmadığında bile durum korunabilir.
Yukarıdaki Alternatif düzenlere sahip içerik hedefleri'nin Oluştur bölümünde, liste ayrıntısı görünümü composable'larının durumunu ListDetailRoute
'ye yükselttik. Böylece, hangi composable'ın görüntülendiğinden bağımsız olarak bu durum korunuyor:
@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
ziyaret edin.Sizin için önerilenler
- Not: JavaScript kapalıyken bağlantı metni gösterilir
- Jetpack Gezinme'yi Gezinme Oluşturma'ya taşıma
- Compose ile gezinme
- Dinamik gezinme ile uyarlanabilir uygulama geliştirme