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