Membangun aplikasi dengan tata letak adaptif

1. Pengantar

Pada codelab sebelumnya, Anda mulai mengubah aplikasi Reply agar adaptif dengan menggunakan class ukuran jendela dan menerapkan navigasi dinamis. Fitur ini merupakan dasar yang penting dan langkah pertama dalam membangun aplikasi untuk semua ukuran layar. Jika melewatkan codelab Membangun aplikasi adaptif dengan navigasi dinamis, Anda sangat disarankan untuk kembali dan memulai dari sana.

Dalam codelab ini, Anda akan membangun konsep yang telah dipelajari untuk menerapkan tata letak adaptif lebih lanjut di aplikasi Anda. Tata letak adaptif yang akan Anda terapkan adalah bagian dari tata letak kanonis - sekumpulan pola yang umum digunakan untuk tampilan layar besar. Anda juga akan mempelajari lebih banyak teknik pengujian dan alat untuk membantu Anda membangun aplikasi yang tangguh dengan cepat.

Prasyarat

  • Menyelesaikan codelab Membangun aplikasi adaptif dengan navigasi dinamis
  • Memahami pemrograman Kotlin, termasuk class, fungsi, dan kondisional
  • Memahami class ViewModel
  • Memahami fungsi Composable
  • Pengalaman membuat tata letak dengan Jetpack Compose
  • Pengalaman menjalankan aplikasi di perangkat atau emulator
  • Pengalaman menggunakan WindowSizeClass API

Yang akan Anda pelajari

  • Cara membuat tata letak adaptif pola tampilan daftar menggunakan Jetpack Compose
  • Cara membuat pratinjau untuk berbagai ukuran layar
  • Cara menguji kode untuk beberapa ukuran layar

Yang akan Anda bangun

  • Anda akan terus mengupdate aplikasi Reply agar adaptif untuk semua ukuran layar.

Aplikasi final akan terlihat seperti ini:

Yang akan Anda butuhkan

  • Komputer dengan akses internet, browser web, dan Android Studio
  • Akses ke GitHub

Mendownload kode awal

Untuk memulai, download kode awal:

Atau, Anda dapat membuat clone repositori GitHub untuk kode tersebut:

$ git clone
https://github.com/google-developer-training/basic-android-kotlin-compose-training-reply-app.git
$ cd basic-android-kotlin-compose-training-reply-app
$ git checkout nav-update

Anda dapat menjelajahi kode awal di repositori GitHub Reply.

2. Pratinjau untuk berbagai ukuran layar

Membuat pratinjau untuk berbagai ukuran layar

Dalam codelab Membangun aplikasi adaptif dengan navigasi dinamis, Anda telah mempelajari cara menggunakan composable pratinjau untuk membantu proses pengembangan. Untuk aplikasi adaptif, praktik terbaiknya adalah membuat beberapa pratinjau untuk menampilkan aplikasi di berbagai ukuran layar. Dengan beberapa pratinjau, Anda dapat melihat perubahan pada semua ukuran layar sekaligus. Selain itu, pratinjau juga berfungsi sebagai dokumentasi bagi developer lain yang meninjau kode Anda untuk melihat apakah aplikasi Anda kompatibel dengan berbagai ukuran layar.

Sebelumnya, Anda hanya memiliki satu pratinjau yang mendukung layar rapat. Berikutnya, Anda akan menambahkan lebih banyak pratinjau.

Untuk menambahkan pratinjau pada layar berukuran sedang dan yang diperluas, selesaikan langkah-langkah berikut:

  1. Tambahkan pratinjau untuk layar berukuran sedang dengan menetapkan nilai widthDp sedang dalam parameter anotasi Preview dan menentukan nilai WindowWidthSizeClass.Medium sebagai parameter untuk composable ReplyApp.

MainActivity.kt

...
@Preview(showBackground = true, widthDp = 700)
@Composable
fun ReplyAppMediumPreview() {
    ReplyTheme {
        Surface {
            ReplyApp(windowSize = WindowWidthSizeClass.Medium)
        }
    }
}
... 
  1. Tambahkan pratinjau lain untuk layar yang diperluas dengan menetapkan nilai widthDp besar dalam parameter anotasi Preview dan menentukan nilai WindowWidthSizeClass.Expanded sebagai parameter untuk composable ReplyApp.

MainActivity.kt

...
@Preview(showBackground = true, widthDp = 1000)
@Composable
fun ReplyAppExpandedPreview() {
    ReplyTheme {
        Surface {
            ReplyApp(windowSize = WindowWidthSizeClass.Expanded)
        }
    }
}
... 
  1. Bangun pratinjau untuk melihat hal berikut:

5577b1d0fe306e33.png

f624e771b76bbc2.png

3. Mengimplementasikan tata letak konten adaptif

Pengantar tampilan daftar-detail

Anda mungkin memperhatikan bahwa konten pada layar yang diperluas terlihat membentang dan tidak memanfaatkan ruang layar yang tersedia dengan baik.

56cfa13ef31d0b59.png

Anda dapat meningkatkan tata letak ini dengan menerapkan salah satu tata letak kanonis. Tata letak kanonis adalah komposisi layar besar yang berfungsi sebagai titik awal untuk desain dan implementasi. Anda dapat menggunakan tiga tata letak yang tersedia untuk memandu cara mengatur elemen umum dalam aplikasi, tampilan daftar, panel pendukung, dan feed. Setiap tata letak mempertimbangkan kasus penggunaan dan komponen umum untuk memenuhi ekspektasi dan kebutuhan pengguna terkait cara aplikasi beradaptasi di seluruh ukuran layar dan titik henti sementara.

Untuk aplikasi Reply, mari kita implementasikan tampilan detail daftar, karena yang terbaik adalah menjelajahi konten dan melihat detail dengan cepat. Dengan tata letak tampilan daftar-detail, Anda akan membuat panel lain di samping layar daftar email untuk menampilkan detail email. Tata letak ini memungkinkan Anda menggunakan layar yang tersedia untuk menampilkan lebih banyak informasi kepada pengguna dan membuat aplikasi menjadi lebih produktif.

Mengimplementasikan tampilan daftar-detail

Untuk menerapkan tampilan daftar-detail bagi layar yang diperluas, selesaikan langkah-langkah berikut:

  1. Untuk merepresentasikan berbagai jenis tata letak konten, di WindowStateUtils.kt, buat class Enum baru untuk jenis konten yang berbeda. Gunakan nilai LIST_AND_DETAIL saat layar yang diperluas sedang digunakan dan LIST_ONLY jika tidak.

WindowStateUtils.kt

...
enum class ReplyContentType {
    LIST_ONLY, LIST_AND_DETAIL
}
... 
  1. Deklarasikan variabel contentType di ReplyApp.kt dan tetapkan contentType yang sesuai untuk berbagai ukuran jendela guna membantu menentukan pilihan jenis konten yang sesuai, bergantung pada ukuran layar.

ReplyApp.kt

...
import com.example.reply.ui.utils.ReplyContentType
...

    val navigationType: ReplyNavigationType
    val contentType: ReplyContentType

    when (windowSize) {
        WindowWidthSizeClass.Compact -> {
            ...
            contentType = ReplyContentType.LIST_ONLY
        }
        WindowWidthSizeClass.Medium -> {
            ...
            contentType = ReplyContentType.LIST_ONLY
        }
        WindowWidthSizeClass.Expanded -> {
            ...
            contentType = ReplyContentType.LIST_AND_DETAIL
        }
        else -> {
            ...
            contentType = ReplyContentType.LIST_ONLY
        }
    }
... 

Selanjutnya, Anda dapat menggunakan nilai contentType untuk membuat cabang yang berbeda bagi tata letak dalam composable ReplyAppContent.

  1. Di ReplyHomeScreen.kt, tambahkan contentType sebagai parameter ke composable ReplyHomeScreen.

ReplyHomeScreen.kt

...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplyHomeScreen(
    navigationType: ReplyNavigationType,
    contentType: ReplyContentType,
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit,
    onEmailCardPressed: (Email) -> Unit,
    onDetailScreenBackPressed: () -> Unit,
    modifier: Modifier = Modifier
) {
...
  1. Teruskan nilai contentType ke composable ReplyHomeScreen.

ReplyApp.kt

...
    ReplyHomeScreen(
        navigationType = navigationType,
        contentType = contentType,
        replyUiState = replyUiState,
        onTabPressed = { mailboxType: MailboxType ->
            viewModel.updateCurrentMailbox(mailboxType = mailboxType)
            viewModel.resetHomeScreenStates()
        },
        onEmailCardPressed = { email: Email ->
            viewModel.updateDetailsScreenStates(
                email = email
            )
        },
        onDetailScreenBackPressed = {
            viewModel.resetHomeScreenStates()
        },
        modifier = modifier
    )

... 
  1. Tambahkan contentType sebagai parameter untuk composable ReplyAppContent.

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    contentType: ReplyContentType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit),
    onEmailCardPressed: (Email) -> Unit,
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {
... 
  1. Teruskan nilai contentType ke dua composable ReplyAppContent.

ReplyHomeScreen.kt

...
            ReplyAppContent(
                navigationType = navigationType,
                contentType = contentType,
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        }
    } else {
        if (replyUiState.isShowingHomepage) {
            ReplyAppContent(
                navigationType = navigationType,
                contentType = contentType,
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        } else {
            ReplyDetailsScreen(
                replyUiState = replyUiState,
                isFullScreen = true,
                onBackButtonClicked = onDetailScreenBackPressed,
                modifier = modifier
            )
        }
    }
... 

Mari kita tampilkan daftar lengkap dan layar detail saat contentType adalah LIST_AND_DETAIL atau hanya daftar konten email saat contentType adalah LIST_ONLY.

  1. Di ReplyHomeScreen.kt, tambahkan pernyataan if/else di composable ReplyAppContent untuk menampilkan composable ReplyListAndDetailContent saat nilai contentType adalah LIST_AND_DETAIL dan menampilkan composable ReplyListOnlyContent di cabang else.

ReplyHomeScreen.kt

...
        Column(
            modifier = modifier
                .fillMaxSize()
                .background(MaterialTheme.colorScheme.inverseOnSurface)
        ) {
            if (contentType == ReplyContentType.LIST_AND_DETAIL) {
                ReplyListAndDetailContent(
                    replyUiState = replyUiState,
                    onEmailCardPressed = onEmailCardPressed,
                    modifier = Modifier.weight(1f)
                )
            } else {
                ReplyListOnlyContent(
                    replyUiState = replyUiState,
                    onEmailCardPressed = onEmailCardPressed,
                    modifier = Modifier.weight(1f)
                        .padding(
                            horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
                        )
                )
            }
            AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
                ReplyBottomNavigationBar(
                    currentTab = replyUiState.currentMailbox,
                    onTabPressed = onTabPressed,
                    navigationItemContentList = navigationItemContentList
                )
            }
        }
... 
  1. Hapus kondisi replyUiState.isShowingHomepage untuk menampilkan panel navigasi permanen, karena pengguna tidak perlu membuka tampilan detail jika menggunakan tampilan yang diperluas.

ReplyHomeScreen.kt

...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER) {
        PermanentNavigationDrawer(
            drawerContent = {
                PermanentDrawerSheet(Modifier.width(dimensionResource(R.dimen.drawer_width))) {
                    NavigationDrawerContent(
                        selectedDestination = replyUiState.currentMailbox,
                        onTabPressed = onTabPressed,
                        navigationItemContentList = navigationItemContentList,
                        modifier = Modifier
                            .wrapContentWidth()
                            .fillMaxHeight()
                            .background(MaterialTheme.colorScheme.inverseOnSurface)
                            .padding(dimensionResource(R.dimen.drawer_padding_content))
                    )
                }
            }
        ) {

... 
  1. Jalankan aplikasi Anda pada mode tablet untuk melihat layar di bawah:

fe811a212feefea5.png

Meningkatkan elemen UI untuk tampilan daftar-detail

Saat ini, aplikasi Anda menampilkan panel detail di layar utama untuk layar yang diperluas.

e7c540e41fe1c3d.png

Namun, layar berisi elemen yang tidak relevan, seperti tombol kembali, header subjek, dan padding tambahan, karena dirancang untuk layar detail mandiri. Anda dapat meningkatkan fitur ini di waktu berikutnya dengan penyesuaian sederhana.

Guna meningkatkan layar detail untuk tampilan yang diperluas, selesaikan langkah-langkah berikut:

  1. Di ReplyDetailsScreen.kt, tambahkan variabel isFullScreen sebagai parameter Boolean ke composable ReplyDetailsScreen.

Penambahan ini memungkinkan Anda membedakan composable saat menggunakannya sebagai composable mandiri dan saat Anda menggunakannya di layar utama.

ReplyDetailsScreen.kt

...
@Composable
fun ReplyDetailsScreen(
    replyUiState: ReplyUiState,
    onBackPressed: () -> Unit,
    modifier: Modifier = Modifier,
    isFullScreen: Boolean = false
) {
... 
  1. Di dalam composable ReplyDetailsScreen, gabungkan composable ReplyDetailsScreenTopBar dengan pernyataan if sehingga hanya ditampilkan saat aplikasi dalam mode layar penuh.

ReplyDetailsScreen.kt

...
    LazyColumn(
        modifier = modifier
            .fillMaxSize()
            .background(color = MaterialTheme.colorScheme.inverseOnSurface)
            .padding(top = dimensionResource(R.dimen.detail_card_list_padding_top))
    ) {
        item {
            if (isFullScreen) {
                ReplyDetailsScreenTopBar(
                    onBackPressed,
                    replyUiState,
                    Modifier
                        .fillMaxWidth()
                        .padding(bottom = dimensionResource(R.dimen.detail_topbar_padding_bottom))
                    )
                )
            }

... 

Sekarang Anda dapat menambahkan padding. Padding yang diperlukan untuk composable ReplyEmailDetailsCard berbeda-beda bergantung pada apakah Anda menggunakannya sebagai layar penuh atau tidak. Saat Anda menggunakan ReplyEmailDetailsCard dengan composable lain di layar yang diperluas, ada padding tambahan dari composable lain.

  1. Teruskan nilai isFullScreen ke composable ReplyEmailDetailsCard. Teruskan pengubah dengan padding horizontal R.dimen.detail_card_outer_padding_horizontal jika layar dalam mode layar penuh atau teruskan pengubah dengan padding akhir R.dimen.detail_card_outer_padding_horizontal.

ReplyDetailsScreen.kt

...
        item {
            if (isFullScreen) {
                ReplyDetailsScreenTopBar(
                    onBackPressed,
                    replyUiState,
                    Modifier
                        .fillMaxWidth()
                        .padding(bottom = dimensionResource(R.dimen.detail_topbar_padding_bottom))
                    )
                )
            }
            ReplyEmailDetailsCard(
                email = replyUiState.currentSelectedEmail,
                mailboxType = replyUiState.currentMailbox,
                isFullScreen = isFullScreen,
                modifier = if (isFullScreen) {
                    Modifier.padding(horizontal = dimensionResource(R.dimen.detail_card_outer_padding_horizontal))
                } else {
                    Modifier.padding(end = dimensionResource(R.dimen.detail_card_outer_padding_horizontal))
                }
            )
        }
... 
  1. Tambahkan nilai isFullScreen sebagai parameter ke composable ReplyEmailDetailsCard.

ReplyDetailsScreen.kt

...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ReplyEmailDetailsCard(
    email: Email,
    mailboxType: MailboxType,
    modifier: Modifier = Modifier,
    isFullScreen: Boolean = false
) {
... 
  1. Di dalam composable ReplyEmailDetailsCard, hanya tampilkan teks subjek email saat aplikasi tidak dalam mode layar penuh, karena tata letak layar penuh sudah menampilkan subjek email sebagai header. Jika menggunakan layar penuh, tambahkan pengatur jarak dengan tinggi R.dimen.detail_content_padding_top.

ReplyDetailsScreen.kt

...
Column(
    modifier = Modifier
        .fillMaxWidth()
        .padding(dimensionResource(R.dimen.detail_card_inner_padding))
) {
    DetailsScreenHeader(
        email,
        Modifier.fillMaxWidth()
    )
    if (isFullScreen) {
        Spacer(modifier = Modifier.height(dimensionResource(R.dimen.detail_content_padding_top)))
    } else {
        Text(
            text = stringResource(email.subject),
            style = MaterialTheme.typography.bodyMedium,
            color = MaterialTheme.colorScheme.outline,
            modifier = Modifier.padding(
                top = dimensionResource(R.dimen.detail_content_padding_top),
                bottom = dimensionResource(R.dimen.detail_expanded_subject_body_spacing)
            ),
        )
    }
    Text(
        text = stringResource(email.body),
        style = MaterialTheme.typography.bodyLarge,
        color = MaterialTheme.colorScheme.onSurfaceVariant,
    )
    DetailsScreenButtonBar(mailboxType, displayToast)
}

... 
  1. Di ReplyHomeScreen.kt, di dalam composable ReplyHomeScreen, teruskan nilai true untuk parameter isFullScreen saat membuat composable ReplyDetailsScreen dalam mode mandiri.

ReplyHomeScreen.kt

...
        } else {
            ReplyDetailsScreen(
                replyUiState = replyUiState,
                isFullScreen = true,
                onBackPressed = onDetailScreenBackPressed,
                modifier = modifier
            )
        }
... 
  1. Jalankan aplikasi dalam mode tablet dan lihat tata letak berikut:

833b3986a71a0b67.png

Menyesuaikan penanganan kembali untuk tampilan daftar-detail

Dengan layar yang diperluas, Anda tidak perlu menavigasi ke ReplyDetailsScreen sama sekali. Sebagai gantinya, Anda ingin aplikasi menutup saat pengguna memilih tombol kembali. Dengan demikian, kita harus menyesuaikan pengendali kembali.

Ubah pengendali kembali dengan meneruskan fungsi activity.finish() sebagai parameter onBackPressed dari composable ReplyDetailsScreen di dalam composable ReplyListAndDetailContent.

ReplyHomeContent.kt

...
import android.app.Activity
import androidx.compose.ui.platform.LocalContext
...
        val activity = LocalContext.current as Activity
        ReplyDetailsScreen(
            replyUiState = replyUiState,
            modifier = Modifier.weight(1f),
            onBackPressed = { activity.finish() }
        )
... 

4. Verifikasi untuk berbagai ukuran layar

Pedoman kualitas aplikasi perangkat layar besar

Untuk menciptakan pengalaman yang luar biasa dan konsisten bagi pengguna Android, penting untuk membangun dan menguji aplikasi Anda dengan mempertimbangkan kualitas. Anda dapat membaca Pedoman kualitas aplikasi inti untuk menentukan cara meningkatkan kualitas aplikasi Anda.

Agar dapat membangun aplikasi yang bagus untuk semua faktor bentuk, tinjau Pedoman kualitas aplikasi perangkat layar besar. Aplikasi Anda juga harus memenuhi Tingkat 3 - Persyaratan kesiapan perangkat layar besar.

Menguji aplikasi Anda secara manual untuk mengetahui kesiapan perangkat layar besar

Pedoman kualitas aplikasi memberikan rekomendasi dan prosedur perangkat pengujian untuk memeriksa kualitas aplikasi Anda. Mari kita lihat contoh pengujian yang relevan dengan aplikasi Reply.

Deskripsi kualitas aplikasi perangkat layar besar untuk konfigurasi dan kontinuitas.

Pedoman kualitas aplikasi di atas mengharuskan aplikasi untuk mempertahankan atau memulihkan statusnya setelah konfigurasi berubah. Panduan ini juga memberikan petunjuk tentang cara menguji aplikasi, seperti yang ditampilkan dalam gambar berikut:

Langkah-langkah pengujian kualitas aplikasi perangkat layar besar untuk konfigurasi dan kontinuitas.

Guna menguji aplikasi Reply secara manual untuk kontinuitas konfigurasi, selesaikan langkah-langkah berikut:

  1. Jalankan aplikasi Reply pada perangkat berukuran sedang atau, jika Anda menggunakan emulator yang dapat diubah ukurannya, dalam mode perangkat foldable yang dibentangkan.
  2. Pastikan Putar otomatis di emulator disetel ke aktif.

5a1c3a4cb4fc0192.png

  1. Scroll daftar email ke bawah.

7ce0887b5b38a1f0.png

  1. Klik kartu email. Misalnya, buka email dari Ali.

16d7ca9c17206bf8.png

  1. Putar perangkat untuk memeriksa apakah email yang dipilih masih konsisten dengan email yang dipilih dalam orientasi potret. Dalam contoh ini, email dari Ali masih ditampilkan.

d078601f2cc50341.png

  1. Putar kembali ke orientasi potret untuk memeriksa apakah aplikasi masih menampilkan email yang sama.

16d7ca9c17206bf8.png

5. Menambahkan pengujian otomatis untuk aplikasi adaptif

Mengonfigurasi pengujian untuk ukuran layar yang rapat

Di codelab Menguji Aplikasi Cupcake, Anda telah mempelajari cara membuat pengujian UI. Sekarang, mari pelajari cara membuat pengujian tertentu untuk berbagai ukuran layar.

Di aplikasi Reply, Anda menggunakan elemen navigasi yang berbeda untuk ukuran layar yang berbeda. Misalnya, Anda akan melihat panel navigasi permanen saat pengguna melihat layar yang diperluas. Hal ini berguna untuk membuat pengujian guna memverifikasi keberadaan berbagai elemen navigasi, seperti navigasi bawah, kolom samping navigasi, dan panel navigasi untuk berbagai ukuran layar.

Guna membuat pengujian untuk memverifikasi keberadaan elemen navigasi bawah dalam layar yang rapat, lakukan langkah-langkah berikut:

  1. Dalam direktori pengujian, buat class Kotlin baru bernama ReplyAppTest.kt.
  2. Di class ReplyAppTest, buat aturan pengujian menggunakan createAndroidComposeRule dan teruskan ComponentActivity sebagai parameter jenis. ComponentActivity digunakan untuk mengakses aktivitas kosong, bukan MainActivity.

ReplyAppTest.kt

...
class ReplyAppTest {

    @get:Rule
    val composeTestRule = createAndroidComposeRule<ComponentActivity>()
...

Untuk membedakan antara elemen navigasi di layar, tambahkan testTag di composable ReplyBottomNavigationBar.

  1. Tentukan resource string untuk Navigation Bottom.

strings.xml

...
<resources>
...
    <string name="navigation_bottom">Navigation Bottom</string>
...
</resources>
  1. Tambahkan nama string sebagai argumen testTag untuk metode testTag dari Modifier dalam composable ReplyBottomNavigationBar.

ReplyHomeScreen.kt

...
val bottomNavigationContentDescription = stringResource(R.string.navigation_bottom)
ReplyBottomNavigationBar(
    ...
    modifier = Modifier
        .fillMaxWidth()
        .testTag(bottomNavigationContentDescription)
)
...
  1. Di class ReplyAppTest, buat fungsi pengujian untuk menguji layar berukuran rapat. Setel konten composeTestRule dengan composable ReplyApp dan teruskan WindowWidthSizeClass.Compact sebagai argumen windowSize.

ReplyAppTest.kt

...
    @Test
    fun compactDevice_verifyUsingBottomNavigation() {
        // Set up compact window
        composeTestRule.setContent {
            ReplyApp(
                windowSize = WindowWidthSizeClass.Compact
            )
        }
    }
  1. Nyatakan bahwa elemen navigasi bawah ada dengan tag pengujian. Panggil fungsi ekstensi onNodeWithTagForStringId di composeTestRule dan teruskan string bawah navigasi, lalu panggil metode assertExists().

ReplyAppTest.kt

...
    @Test
    fun compactDevice_verifyUsingBottomNavigation() {
        // Set up compact window
        composeTestRule.setContent {
            ReplyApp(
                windowSize = WindowWidthSizeClass.Compact
            )
        }
        // Bottom navigation is displayed
        composeTestRule.onNodeWithTagForStringId(
            R.string.navigation_bottom
        ).assertExists()
    }
  1. Jalankan pengujian dan pastikan pengujian berhasil.

Mengonfigurasi pengujian untuk ukuran layar sedang dan diperluas

Setelah Anda berhasil membuat pengujian untuk layar yang rapat, mari kita buat pengujian yang sesuai untuk layar berukuran sedang dan yang diperluas.

Untuk membuat pengujian guna memverifikasi keberadaan kolom samping navigasi dan panel navigasi permanen bagi layar berukuran sedang dan yang diperluas, lakukan langkah-langkah berikut:

  1. Tentukan resource string untuk Kolom Samping Navigasi yang akan digunakan sebagai tag pengujian di lain waktu.

strings.xml

...
<resources>
...
    <string name="navigation_rail">Navigation Rail</string>
...
</resources>
  1. Teruskan string sebagai tag pengujian melalui Modifier di composable PermanentNavigationDrawer.

ReplyHomeScreen.kt

...
    val navigationDrawerContentDescription = stringResource(R.string.navigation_drawer)
        PermanentNavigationDrawer(
...
modifier = Modifier.testTag(navigationDrawerContentDescription)
)
...
  1. Teruskan string sebagai tag pengujian melalui Modifier dalam composable ReplyNavigationRail.

ReplyHomeScreen.kt

...
val navigationRailContentDescription = stringResource(R.string.navigation_rail)
ReplyNavigationRail(
    ...
    modifier = Modifier
        .testTag(navigationRailContentDescription)
)
...
  1. Tambahkan pengujian untuk memverifikasi bahwa elemen rel navigasi ada di layar sedang.

ReplyAppTest.kt

...
@Test
fun mediumDevice_verifyUsingNavigationRail() {
    // Set up medium window
    composeTestRule.setContent {
        ReplyApp(
            windowSize = WindowWidthSizeClass.Medium
        )
    }
    // Navigation rail is displayed
    composeTestRule.onNodeWithTagForStringId(
        R.string.navigation_rail
    ).assertExists()
}
  1. Tambahkan pengujian untuk memverifikasi bahwa elemen panel navigasi ada di layar yang diperluas.

ReplyAppTest.kt

...
@Test
fun expandedDevice_verifyUsingNavigationDrawer() {
    // Set up expanded window
    composeTestRule.setContent {
        ReplyApp(
            windowSize = WindowWidthSizeClass.Expanded
        )
    }
    // Navigation drawer is displayed
    composeTestRule.onNodeWithTagForStringId(
        R.string.navigation_drawer
    ).assertExists()
}
  1. Gunakan emulator tablet atau emulator yang dapat diubah ukurannya dalam mode Tablet untuk menjalankan pengujian.
  2. Jalankan semua pengujian dan pastikan pengujian berhasil.

Menguji perubahan konfigurasi di layar yang rapat

Perubahan konfigurasi adalah kejadian umum yang terjadi dalam siklus proses aplikasi Anda. Misalnya, saat Anda mengubah orientasi dari potret ke lanskap, perubahan konfigurasi terjadi. Saat terjadi perubahan konfigurasi, penting untuk menguji apakah aplikasi Anda mempertahankan statusnya. Berikutnya, Anda akan membuat pengujian, yang menyimulasikan perubahan konfigurasi, untuk menguji apakah aplikasi mempertahankan statusnya di layar yang rapat.

Untuk menguji perubahan konfigurasi di layar yang rapat:

  1. Dalam direktori pengujian, buat class Kotlin baru bernama ReplyAppStateRestorationTest.kt.
  2. Di class ReplyAppStateRestorationTest, buat aturan pengujian menggunakan createAndroidComposeRule dan teruskan ComponentActivity sebagai parameter jenis.

ReplyAppStateRestorationTest.kt

...
class ReplyAppStateRestorationTest {

    /**
     * Note: To access to an empty activity, the code uses ComponentActivity instead of
     * MainActivity.
     */
    @get:Rule
    val composeTestRule = createAndroidComposeRule<ComponentActivity>()
}
...
  1. Buat fungsi pengujian untuk memverifikasi bahwa email masih dipilih di layar rapat setelah perubahan konfigurasi.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    
}
...

Untuk menguji perubahan konfigurasi, Anda harus menggunakan StateRestorationTester.

  1. Siapkan stateRestorationTester dengan meneruskan composeTestRule sebagai argumen ke StateRestorationTester.
  2. Gunakan setContent() dengan composable ReplyApp dan teruskan WindowWidthSizeClass.Compact sebagai argumen windowSize.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup compact window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }

}
...
  1. Pastikan bahwa email ketiga ditampilkan di aplikasi. Gunakan metode assertIsDisplayed() di composeTestRule, yang mencari teks email ketiga.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup compact window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()
}
...
  1. Buka layar detail email dengan mengklik subjek email. Gunakan metode performClick() untuk menavigasi.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup compact window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()

    // Open detailed page
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()
}
...
  1. Pastikan bahwa email ketiga ditampilkan di layar detail. Nyatakan keberadaan tombol kembali untuk mengonfirmasi bahwa aplikasi berada di layar detail, dan pastikan bahwa teks email ketiga ditampilkan.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    ...
    // Open detailed page
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()

    // Verify that it shows the detailed screen for the correct email
    composeTestRule.onNodeWithContentDescriptionForStringId(
        R.string.navigation_back
    ).assertExists()
    composeTestRule.onNodeWithText(
}
...
  1. Simulasikan perubahan konfigurasi menggunakan stateRestorationTester.emulateSavedInstanceStateRestore().

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    ...
    // Verify that it shows the detailed screen for the correct email
    composeTestRule.onNodeWithContentDescriptionForStringId(
        R.string.navigation_back
    ).assertExists()
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertExists()

    // Simulate a config change
    stateRestorationTester.emulateSavedInstanceStateRestore()
}
...
  1. Pastikan kembali bahwa email ketiga ditampilkan di layar detail. Nyatakan keberadaan tombol kembali untuk mengonfirmasi bahwa aplikasi berada di layar detail, dan pastikan bahwa teks email ketiga ditampilkan.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup compact window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()

    // Open detailed page
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()

    // Verify that it shows the detailed screen for the correct email
    composeTestRule.onNodeWithContentDescriptionForStringId(
        R.string.navigation_back
    ).assertExists()
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertExists()

    // Simulate a config change
    stateRestorationTester.emulateSavedInstanceStateRestore()

    // Verify that it still shows the detailed screen for the same email
    composeTestRule.onNodeWithContentDescriptionForStringId(
        R.string.navigation_back
    ).assertExists()
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertExists()
}

...
  1. Jalankan pengujian dengan emulator ponsel atau emulator yang dapat diubah ukurannya dalam mode Ponsel.
  2. Pastikan pengujian berhasil.

Menguji perubahan konfigurasi di layar yang diperluas

Untuk menguji perubahan konfigurasi di layar yang diperluas dengan menyimulasikan perubahan konfigurasi dan meneruskan WindowWidthSizeClass yang sesuai, selesaikan langkah-langkah berikut:

  1. Buat fungsi pengujian untuk memverifikasi bahwa email masih dipilih di layar detail setelah perubahan konfigurasi.

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {

}
...

Untuk menguji perubahan konfigurasi, Anda harus menggunakan StateRestorationTester.

  1. Siapkan stateRestorationTester dengan meneruskan composeTestRule sebagai argumen ke StateRestorationTester.
  2. Gunakan setContent() dengan composable ReplyApp dan teruskan WindowWidthSizeClass.Expanded sebagai argumen windowSize.

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup expanded window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }
}
...
  1. Pastikan bahwa email ketiga ditampilkan di aplikasi. Gunakan metode assertIsDisplayed() di composeTestRule, yang mencari teks email ketiga.

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup expanded window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()
}
...
  1. Pilih email ketiga di layar detail. Gunakan metode performClick() untuk memilih email.

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup expanded window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()

    // Select third email
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()
    ...
}

...
  1. Verifikasi bahwa layar detail menampilkan email ketiga dengan menggunakan testTag pada layar detail dan mencari teks pada turunannya. Pendekatan ini memastikan bahwa Anda dapat menemukan teks di bagian detail dan bukan di daftar email.

ReplyAppStateRestorationTest.kt

...

@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    ...
    // Select third email
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()

    // Verify that third email is displayed on the details screen
    composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
        .assertAny(hasAnyDescendant(hasText(
            composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
        )
...
}

...
  1. Simulasikan perubahan konfigurasi menggunakan stateRestorationTester.emulateSavedInstanceStateRestore().

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    ...
    // Verify that third email is displayed on the details screen
    composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
        .assertAny(hasAnyDescendant(hasText(
            composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
        )

    // Simulate a config change
    stateRestorationTester.emulateSavedInstanceStateRestore()
    ...
}
...
  1. Pastikan kembali bahwa layar detail menampilkan email ketiga setelah perubahan konfigurasi.

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup expanded window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()

    // Select third email
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()

    // Verify that third email is displayed on the details screen
    composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
        .assertAny(hasAnyDescendant(hasText(
            composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
        )

    // Simulate a config change
    stateRestorationTester.emulateSavedInstanceStateRestore()

    // Verify that third email is still displayed on the details screen
    composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
        .assertAny(hasAnyDescendant(hasText(
            composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
        )
}
...
  1. Jalankan pengujian dengan emulator tablet atau emulator yang dapat diubah ukurannya dalam mode Tablet.
  2. Pastikan pengujian berhasil.

Menggunakan anotasi untuk mengelompokkan pengujian pada berbagai ukuran layar

Anda mungkin menyadari dari pengujian sebelumnya bahwa beberapa pengujian gagal saat dijalankan di perangkat dengan ukuran layar yang tidak kompatibel. Meskipun dapat menjalankan pengujian satu per satu menggunakan perangkat yang sesuai, pendekatan ini mungkin tidak diskalakan jika ada banyak kasus pengujian.

Untuk mengatasi masalah ini, Anda dapat membuat anotasi untuk menunjukkan ukuran layar tempat pengujian dapat berjalan, dan mengonfigurasi pengujian yang dianotasi untuk perangkat yang sesuai.

Untuk menjalankan pengujian berdasarkan ukuran layar, selesaikan langkah-langkah berikut:

  1. Dalam direktori pengujian, buat TestAnnotations.kt, yang berisi tiga class anotasi: TestCompactWidth, TestMediumWidth, TestExpandedWidth.

TestAnnotations.kt

...
annotation class TestCompactWidth
annotation class TestMediumWidth
annotation class TestExpandedWidth
...
  1. Gunakan anotasi pada fungsi pengujian untuk pengujian rapat dengan menempatkan anotasi TestCompactWidth setelah anotasi pengujian untuk pengujian rapat dalam ReplyAppTest dan ReplyAppStateRestorationTest.

ReplyAppTest.kt

...
    @Test
    @TestCompactWidth
    fun compactDevice_verifyUsingBottomNavigation() {
...

ReplyAppStateRestorationTest.kt

...
    @Test
    @TestCompactWidth
    fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {

...
  1. Gunakan anotasi pada fungsi pengujian untuk pengujian sedang dengan menempatkan anotasi TestMediumWidth setelah anotasi pengujian untuk pengujian sedang di ReplyAppTest.

ReplyAppTest.kt

...
    @Test
    @TestMediumWidth
    fun mediumDevice_verifyUsingNavigationRail() {
...
  1. Gunakan anotasi pada fungsi pengujian untuk pengujian yang diperluas dengan menempatkan anotasi TestExpandedWidth setelah anotasi pengujian untuk pengujian yang diperluas di ReplyAppTest dan ReplyAppStateRestorationTest.

ReplyAppTest.kt

...
    @Test
    @TestExpandedWidth
    fun expandedDevice_verifyUsingNavigationDrawer() {
...

ReplyAppStateRestorationTest.kt

...
    @Test
    @TestExpandedWidth
    fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
...

Untuk memastikan keberhasilan, konfigurasi pengujian agar hanya menjalankan pengujian yang dianotasi dengan TestCompactWidth.

  1. Di Android Studio, pilih Run > Edit Configurations... 7be537f5faa1a61a.png
  2. Ganti nama pengujian sebagai Compact tests, lalu pilih untuk menjalankan pengujian All in Package.

f70b74bc2e6674f1.png

  1. Klik titik tiga (...) di sebelah kanan kolom Instrumentation arguments.
  2. Klik tombol plus (+) dan tambahkan parameter tambahan: annotation dengan nilai com.example.reply.test.TestCompactWidth.

cf1ef9b80a1df8aa.png

  1. Jalankan pengujian dengan emulator rapat.
  2. Pastikan hanya pengujian rapat yang dijalankan.

204ed40031f8615a.png

  1. Ulangi langkah-langkah tersebut untuk layar sedang dan yang diperluas.

6. Mendapatkan kode solusi

Untuk mendownload kode codelab yang sudah selesai, gunakan perintah git berikut:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-reply-app.git

Atau, Anda dapat mendownload repositori sebagai file ZIP, lalu mengekstraknya, dan membukanya di Android Studio.

Jika Anda ingin melihat kode solusi, lihat di GitHub.

7. Kesimpulan

Selamat! Anda telah membuat aplikasi Reply menjadi adaptif untuk semua ukuran layar dengan menerapkan tata letak adaptif. Anda juga belajar mempercepat pengembangan menggunakan pratinjau dan mempertahankan kualitas aplikasi menggunakan berbagai metode pengujian.

Jangan lupa untuk membagikan karya Anda di media sosial dengan #AndroidBasics.

Pelajari lebih lanjut