Navigasi adalah proses berinteraksi dengan UI aplikasi untuk mengakses tujuan konten aplikasi. Prinsip navigasi Android memberikan panduan yang membantu Anda membuat navigasi aplikasi yang konsisten dan intuitif.
UI yang responsif memberikan tujuan konten yang responsif dan sering menyertakan jenis elemen navigasi yang berbeda sebagai respons terhadap perubahan ukuran layar—misalnya, menu navigasi bawah di layar kecil, kolom samping navigasi di layar berukuran sedang, atau panel navigasi yang persisten di layar besar—tetapi UI yang responsif harus tetap mematuhi prinsip navigasi.
Komponen Navigasi Jetpack menerapkan prinsip navigasi dan dapat digunakan untuk memfasilitasi pengembangan aplikasi dengan UI yang responsif.
Navigasi UI responsif
Ukuran jendela tampilan yang ditempati oleh aplikasi memengaruhi ergonomi dan kegunaan. Class ukuran jendela memungkinkan Anda menentukan elemen navigasi yang sesuai (seperti menu navigasi, kolom samping, atau panel samping) dan menempatkannya di tempat yang paling mudah diakses oleh pengguna. Dalam pedoman tata letak Desain Material, elemen navigasi menempati ruang tetap di tepi depan layar dan dapat dipindahkan ke tepi bawah jika lebar aplikasinya rapat. Pilihan elemen navigasi sangat bergantung pada ukuran jendela aplikasi dan jumlah item yang harus disimpan elemen.
Class ukuran jendela | Sedikit item | Banyak item |
---|---|---|
lebar rapat | menu navigasi bawah | panel navigasi (tepi depan atau bawah) |
lebar sedang | kolom samping navigasi | panel navigasi (tepi depan) |
lebar diperluas | kolom samping navigasi | panel navigasi persisten (tepi depan) |
Dalam tata letak berbasis tampilan, file resource tata letak dapat dikualifikasikan menurut titik henti sementara class ukuran jendela agar menggunakan elemen navigasi yang berbeda untuk dimensi tampilan yang berbeda. Jetpack Compose dapat menggunakan titik henti sementara yang disediakan oleh window size class API untuk secara terprogram menentukan elemen navigasi yang paling sesuai untuk jendela aplikasi.
View
<!-- 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>
Compose
// 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 } ) }
Tujuan konten responsif
Pada UI yang responsif, tata letak setiap tujuan konten harus menyesuaikan dengan perubahan ukuran jendela. Aplikasi Anda dapat menyesuaikan spasi tata letak, memosisikan ulang elemen, menambahkan atau menghapus konten, atau mengubah elemen UI, seperti elemen navigasi. (Lihat Memigrasikan UI ke tata letak responsif dan Mendukung berbagai ukuran layar.)
Jika setiap tujuan dapat dengan baik menangani peristiwa perubahan ukuran, perubahan akan diisolasi ke UI. Status aplikasi lainnya, seperti navigasi, tidak akan terpengaruh.
Navigasi tidak boleh terjadi sebagai efek samping dari perubahan ukuran jendela. Jangan membuat tujuan konten hanya untuk mengakomodasi ukuran jendela yang berbeda. Misalnya, jangan membuat tujuan konten yang berbeda untuk layar perangkat foldable yang berbeda.
Menavigasi sebagai efek samping dari perubahan ukuran jendela memiliki masalah berikut:
- Tujuan lama (untuk ukuran jendela sebelumnya) mungkin akan terlihat sesaat sebelum menavigasi ke tujuan baru
- Agar tetap dapat dikembalikan ke setelan semula (misalnya, saat perangkat dilipat dan dibentangkan), navigasi diperlukan untuk setiap ukuran jendela
- Mempertahankan status aplikasi antartujuan bisa jadi tidak mudah karena navigasi dapat merusak status setelah memunculkan data sebelumnya
Selain itu, aplikasi Anda mungkin tidak berada di latar depan saat perubahan ukuran jendela berlangsung. Tata letak aplikasi Anda mungkin memerlukan lebih banyak ruang daripada aplikasi latar depan, dan ketika pengguna kembali ke aplikasi Anda, orientasi dan ukuran jendela semuanya bisa saja telah berubah.
Jika aplikasi Anda memerlukan tujuan konten unik berdasarkan ukuran jendela, sebaiknya Anda menggabungkan tujuan yang relevan ke dalam satu tujuan yang menyertakan tata letak alternatif.
Tujuan konten dengan tata letak alternatif
Sebagai bagian dari desain yang responsif, satu tujuan navigasi dapat memiliki tata letak alternatif bergantung pada ukuran jendela aplikasi. Setiap tata letak menggunakan seluruh jendela, tetapi tata letak yang berbeda disajikan untuk ukuran jendela yang berbeda.
Contoh kanonis adalah tampilan detail daftar. Untuk ukuran jendela kecil, aplikasi Anda akan menampilkan satu tata letak konten untuk daftar dan satu tata letak untuk detailnya. Menavigasi ke tujuan tampilan daftar-detail awalnya hanya akan menampilkan tata letak daftar. Saat item daftar dipilih, aplikasi Anda akan menampilkan tata letak detail sehingga daftar akan digantikan. Saat kontrol kembali dipilih, tata letak daftar akan ditampilkan sehingga detail akan digantikan. Namun, untuk ukuran jendela yang diperluas, tata letak daftar dan detail akan ditampilkan berdampingan.
View
SlidingPaneLayout
memungkinkan Anda membuat satu tujuan navigasi yang menampilkan dua panel konten secara berdampingan di layar besar, tetapi di perangkat kecil seperti ponsel, hanya satu panel yang dapat ditampilkan.
<!-- 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>
Lihat Membuat tata letak dua panel untuk detail cara
mengimplementasikan tata letak daftar-detail menggunakan SlidingPaneLayout
.
Compose
Di Compose, tampilan daftar-detail dapat diterapkan dengan menggabungkan beberapa composable alternatif dalam satu rute yang menggunakan class ukuran jendela untuk menampilkan composable yang sesuai untuk setiap class ukuran.
Rute adalah jalur navigasi ke tujuan konten, yang biasanya adalah composable tunggal, tetapi juga bisa berupa beberapa composable alternatif. Logika bisnis menentukan composable alternatif yang ditampilkan. Composable mengisi jendela aplikasi, apa pun alternatif yang ditampilkan.
Tampilan detail daftar terdiri dari tiga composable, misalnya:
/* 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)
}
}
Satu rute navigasi memberikan akses ke tampilan daftar-detail:
@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
(tujuan navigasi) menentukan mana dari tiga composable yang akan ditampilkan: ListAndDetail
untuk ukuran jendela yang diperluas; ListOfItems
atau ItemDetail
untuk ukuran rapat, bergantung apakah item daftar telah dipilih atau belum.
Rute disertakan dalam NavHost
, misalnya:
NavHost(navController = navController, startDestination = "listDetailRoute") {
composable("listDetailRoute") {
ListDetailRoute(isExpandedWindowSize = isExpandedWindowSize,
selectedItemId = selectedItemId)
}
/*...*/
}
Anda dapat memberikan argumen isExpandedWindowSize
dengan memeriksa WindowMetrics aplikasi Anda.
Argumen selectedItemId
dapat diberikan oleh ViewModel
yang mempertahankan status di semua ukuran jendela. Saat pengguna memilih item dari daftar, variabel status selectedItemId
akan diperbarui:
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,
/*...*/
)
}
}
}
Rute juga menyertakan BackHandler
kustom saat composable detail item mengisi seluruh jendela aplikasi:
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,
/*...*/
)
}
}
}
Menggabungkan status aplikasi dari ViewModel
dengan informasi class ukuran jendela membuat pemilihan composable yang sesuai menjadi lebih masuk akal. Dengan mempertahankan aliran data searah, aplikasi Anda dapat sepenuhnya menggunakan ruang tampilan yang tersedia sekaligus mempertahankan status aplikasi.
Untuk implementasi tampilan daftar-detail lengkap di Compose, lihat contoh JetNews di GitHub.
Satu grafik navigasi
Untuk memberikan pengalaman pengguna yang konsisten di semua ukuran jendela atau perangkat, gunakan satu grafik navigasi jika setiap tujuan konten tersebut memiliki tata letak yang responsif.
Jika Anda menggunakan grafik navigasi yang berbeda untuk setiap class ukuran jendela, setiap kali aplikasi bertransisi dari satu class ukuran ke class ukuran lainnya, Anda harus menentukan tujuan pengguna saat ini di grafik lain, membuat data sebelumnya, dan merekonsiliasi informasi status yang berbeda di antara grafik.
Host navigasi bertingkat
Aplikasi Anda mungkin menyertakan tujuan konten yang memiliki tujuan kontennya sendiri. Misalnya, dalam tampilan daftar-detail, panel detail item bisa menyertakan elemen UI yang menavigasi ke konten yang menggantikan detail item.
Untuk menerapkan jenis sub-navigasi ini, panel detail dapat menjadi host navigasi bertingkat menggunakan grafik navigasinya sendiri yang menentukan tujuan yang diakses dari panel detail:
View
<!-- 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>
Compose
@Composable fun ItemDetail(selectedItemId: String? = null) { val navController = rememberNavController() NavHost(navController, "itemSubdetail1") { composable("itemSubdetail1") { ItemSubdetail1(...) } composable("itemSubdetail2") { ItemSubdetail2(...) } composable("itemSubdetail3") { ItemSubdetail3(...) } } }
Ini berbeda dengan grafik navigasi bertingkat karena grafik navigasi NavHost
bertingkat tidak terhubung ke grafik navigasi utama sehingga Anda tidak dapat menavigasi langsung dari tujuan dalam satu grafik ke tujuan di grafik lain.
Untuk informasi selengkapnya, lihat Grafik navigasi bertingkat dan Menavigasi dengan Compose.
Status yang dipertahankan
Untuk memberikan tujuan konten yang responsif, aplikasi harus mempertahankan statusnya saat perangkat diputar atau dilipat, atau jendela aplikasi diubah ukurannya. Secara default, perubahan konfigurasi seperti ini akan membuat ulang aktivitas aplikasi, fragmen, hierarki tampilan, dan composable. Cara yang direkomendasikan untuk menyimpan status UI adalah dengan ViewModel
atau rememberSaveable
karena akan tetap ada meskipun terjadi perubahan konfigurasi. (Lihat Menyimpan status UI dan Status dan Jetpack Compose.)
Perubahan ukuran harus dapat dikembalikan ke setelan semula, misalnya, saat pengguna memutar perangkat lalu memutarnya kembali.
Tata letak yang responsif dapat menampilkan bagian konten yang berbeda dengan ukuran jendela yang berbeda. Dengan demikian, tata letak yang responsif sering kali perlu menyimpan status tambahan yang berkaitan dengan konten, meskipun status tersebut tidak berlaku untuk ukuran jendela saat ini. Misalnya, tata letak mungkin memiliki ruang untuk menampilkan widget scroll tambahan hanya pada lebar jendela yang lebih besar. Jika perubahan ukuran menyebabkan lebar jendela menjadi terlalu kecil, widget akan disembunyikan. Jika ukuran aplikasi berubah menjadi dimensi sebelumnya, widget scroll akan terlihat lagi, dan posisi scroll asli harus dipulihkan.
Cakupan ViewModel
Panduan developer Bermigrasi ke komponen Navigasi merekomendasikan arsitektur aktivitas tunggal tempat tujuan diterapkan sebagai fragmen dan model datanya diterapkan menggunakan ViewModel
.
ViewModel
selalu mencakup siklus proses, dan setelah siklus proses tersebut berakhir secara permanen, ViewModel
akan dihapus dan dapat dibuang. Siklus proses yang mencakup ViewModel
—dengan demikian, seberapa luas ViewModel
dapat dibagikan—bergantung pada delegasi properti yang digunakan untuk mendapatkan ViewModel
.
Dalam kasus yang paling sederhana, setiap tujuan navigasi adalah satu fragmen dengan status UI yang sepenuhnya terisolasi sehingga setiap fragmen dapat menggunakan delegasi properti viewModels()
untuk mendapatkan ViewModel
yang dicakupkan ke fragmen tersebut.
Untuk berbagi status UI antarfragmen, buat cakupan ViewModel
ke aktivitas dengan memanggil activityViewModels()
dalam fragmen (yang setara dengan aktivitas hanya viewModels()
). Ini memungkinkan aktivitas dan fragmen apa pun yang terpasang di aktivitas untuk berbagi instance ViewModel
. Namun, dalam arsitektur satu aktivitas, cakupan ViewModel
ini berlaku secara efektif selama aplikasi berjalan, sehingga ViewModel
tetap ada di memori meskipun tidak ada fragmen yang menggunakannya.
Misalnya grafik navigasi Anda memiliki urutan tujuan fragmen yang mewakili alur checkout, dan status saat ini untuk seluruh pengalaman checkout berada dalam ViewModel
yang dibagikan di antara fragmen. Pencakupan ViewModel
pada aktivitas tidak hanya terlalu luas, tetapi sebenarnya juga mengekspos masalah lain: jika pengguna melalui alur checkout untuk satu pesanan, lalu melaluinya lagi untuk pesanan kedua, kedua pesanan tersebut menggunakan instance ViewModel
checkout yang sama. Sebelum checkout pesanan kedua, Anda harus menghapus data dari pesanan pertama secara manual, dan kesalahan apa pun dapat merugikan pengguna.
Sebagai gantinya, buat cakupan ViewModel
ke grafik navigasi di NavController
saat ini. Buat grafik navigasi bertingkat untuk mengenkapsulasi tujuan yang merupakan bagian dari alur checkout. Kemudian, di setiap tujuan fragmen tersebut, gunakan delegasi properti navGraphViewModels()
dan teruskan ID grafik navigasi untuk mendapatkan ViewModel
bersama. Hal ini memastikan bahwa setelah pengguna keluar dari alur checkout dan grafik navigasi bertingkat berada di luar cakupan, instance ViewModel
yang terkait akan dihapus dan tidak akan digunakan untuk checkout berikutnya.
Cakupan | Delegasi properti | Dapat berbagi ViewModel dengan |
---|---|---|
Fragmen | Fragment.viewModels() |
Hanya fragmen saat ini |
Aktivitas | Activity.viewModels()
|
Aktivitas dan semua fragmen yang disematkan |
Grafik navigasi | Fragment.navGraphViewModels() |
Semua fragmen di grafik navigasi yang sama |
Perhatikan bahwa jika Anda menggunakan host navigasi bertingkat (lihat di atas), tujuan dalam host tersebut tidak dapat berbagi ViewModel
dengan tujuan di luar host saat menggunakan navGraphViewModels()
karena grafik tidak terhubung. Dalam hal ini, Anda dapat menggunakan cakupan aktivitas.
Status yang ditarik
Di Compose, Anda dapat mempertahankan status selama perubahan ukuran jendela dengan penarikan status. Dengan menarik status composable ke posisi yang lebih tinggi di hierarki komposisi, status dapat dipertahankan meskipun composable tidak lagi terlihat.
Di bagian Compose pada Tujuan konten dengan tata letak alternatif di atas, kita menarik status composable tampilan daftar-detail ke ListDetailRoute
sehingga status tersebut dipertahankan, apa pun composable yang akan ditampilkan:
@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?,
) { /*...*/ }
Referensi lainnya
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Memigrasikan Navigasi Jetpack ke Navigation Compose
- Navigasi dengan Compose
- Membangun aplikasi adaptif dengan navigasi dinamis