Membangun dan menguji aplikasi parkir untuk Android Automotive OS

1. Sebelum memulai

Tidak termasuk dalam cakupan

  • Panduan tentang cara membuat aplikasi media (audio - misalnya musik, radio, podcast) untuk Android Auto dan Android Automotive OS. Lihat Membangun aplikasi media untuk mobil guna mengetahui detail tentang cara membangun aplikasi serupa.

Yang akan Anda butuhkan

Yang akan Anda bangun

Dalam codelab ini, Anda akan mempelajari cara memigrasikan aplikasi seluler streaming video yang ada, Road Reels, ke Android Automotive OS.

Versi titik awal aplikasi yang berjalan di ponsel

Versi lengkap aplikasi yang berjalan di emulator Android Automotive OS dengan potongan layar.

Versi titik awal aplikasi yang berjalan di ponsel

Versi lengkap aplikasi yang berjalan di emulator Android Automotive OS dengan potongan layar.

Yang akan Anda pelajari

  • Cara menggunakan emulator Android Automotive OS.
  • Cara membuat perubahan yang diperlukan untuk membuat build Android Automotive OS
  • Asumsi umum yang dibuat ketika mengembangkan aplikasi untuk perangkat seluler yang mungkin rusak saat aplikasi berjalan di Android Automotive OS
  • Berbagai tingkat kualitas untuk aplikasi di mobil
  • Cara menggunakan sesi media untuk memungkinkan aplikasi lain mengontrol pemutaran aplikasi Anda
  • Perbedaan UI sistem dan inset jendela di perangkat Android Automotive OS dibandingkan dengan perangkat seluler

2. Memulai persiapan

Mendapatkan kode

  1. Kode untuk codelab ini dapat ditemukan di direktori build-a-parked-app dalam repositori GitHub car-codelabs. Untuk membuat clone kode ini, jalankan perintah berikut:
git clone https://github.com/android/car-codelabs.git
  1. Atau, Anda dapat mendownload repositori sebagai file ZIP:

Membuka project

  • Setelah memulai Android Studio, impor project dengan memilih direktori build-a-parked-app/start saja. Direktori build-a-parked-app/end berisi kode solusi yang dapat Anda rujuk kapan saja jika Anda mengalami kebuntuan atau hanya ingin melihat project lengkap.

Memahami kode

  • Setelah membuka project di Android Studio, luangkan waktu untuk memeriksa kode awal.

3. Mempelajari aplikasi parkir untuk Android Automotive OS

Aplikasi parkir merupakan subkumpulan kategori aplikasi yang didukung oleh Android Automotive OS. Pada saat penulisan codelab ini, aplikasi tersebut terdiri dari aplikasi streaming video, browser web, dan game. Aplikasi ini sangat cocok untuk mobil, mengingat hardware yang ada di kendaraan yang dilengkapi Google dan meningkatnya prevalensi kendaraan listrik. Waktu pengisian daya memberikan peluang besar bagi pengemudi dan penumpang untuk berinteraksi dengan jenis aplikasi ini.

Dalam banyak hal, mobil mirip dengan perangkat layar besar lainnya seperti tablet dan perangkat foldable. Mobil memiliki layar sentuh dengan ukuran, resolusi, dan rasio aspek yang serupa, dan bisa saja dalam orientasi potret atau lanskap (meskipun, tidak seperti tablet, orientasi layar mobil tetap). Mobil juga merupakan perangkat terhubung yang dapat masuk dan keluar dari koneksi jaringan. Dengan semua hal itu, tidak mengherankan jika aplikasi yang sudah dioptimalkan untuk perangkat layar besar sering kali memerlukan sedikit saja upaya untuk menghadirkan pengalaman pengguna yang sangat baik di mobil.

Serupa dengan perangkat layar besar, ada juga tingkat kualitas aplikasi untuk aplikasi di mobil:

  • Tingkat 3 - Siap digunakan di mobil: Aplikasi Anda kompatibel dengan perangkat layar besar dan dapat digunakan saat mobil diparkir. Meskipun aplikasi ini mungkin tidak memiliki fitur yang dioptimalkan untuk mobil, pengguna dapat menikmati aplikasi ini seperti yang mereka lakukan di perangkat Android layar besar lainnya. Aplikasi seluler yang memenuhi persyaratan ini layak didistribusikan ke mobil secara apa adanya melalui program aplikasi seluler siap digunakan untuk mobil.
  • Tingkat 2 - Dioptimalkan untuk mobil: Aplikasi Anda memberikan pengalaman yang sangat baik pada tampilan stack tengah mobil. Agar dapat digunakan di mobil, aplikasi Anda harus melalui beberapa rekayasa khusus mobil untuk menyertakan kemampuan yang dapat digunakan di seluruh mode mengemudi atau parkir, bergantung pada kategori aplikasi Anda.
  • Tingkat 1 - Dirancang khusus untuk mobil: Aplikasi Anda dibuat agar berfungsi di berbagai hardware pada mobil dan dapat menyesuaikan pengalamannya di seluruh mode mengemudi dan parkir. Aplikasi ini pada tingkat ini memberikan pengalaman pengguna terbaik yang didesain untuk berbagai layar di mobil, seperti konsol tengah, cluster instrumen, dan layar tambahan - seperti tampilan panorama yang terlihat di banyak mobil premium.

4. Menjalankan aplikasi di emulator Android Automotive OS

Menginstal Image Sistem Automotive with Play Store

  1. Pertama, buka SDK Manager di Android Studio, lalu pilih tab SDK Platforms jika belum dipilih. Di pojok kanan bawah jendela SDK Manager, pastikan kotak Show package details dicentang.
  2. Instal salah satu image emulator Automotive with Play Store yang tercantum di Menambahkan Generic System Image (GSI). Image hanya dapat berjalan di komputer dengan arsitektur yang sama (x86/ARM) dengan image itu sendiri.

Membuat Perangkat Virtual Android untuk Android Automotive OS

  1. Setelah membuka Pengelola Perangkat, pilih Automotive di kolom Category di sisi kiri jendela. Kemudian, pilih profil hardware gabungan Automotive (1024p landscape) dari daftar, lalu klik Next.

Wizard Konfigurasi Perangkat Virtual menampilkan profil hardware 'Automotive (1024p landscape)' yang dipilih.

  1. Di halaman berikutnya, pilih image sistem dari langkah sebelumnya. Klik Next dan pilih opsi lanjutan yang Anda inginkan sebelum akhirnya membuat AVD dengan mengklik Finish. Catatan: jika Anda memilih image API 30, image tersebut mungkin berada di tab selain tab Recommended.

Menjalankan aplikasi

Jalankan aplikasi di emulator yang baru saja Anda buat menggunakan konfigurasi run app yang ada. Coba gunakan aplikasi di berbagai layar dan bandingkan perilakunya dengan ketika dijalankan di emulator ponsel atau tablet.

301e6c0d3675e937.png

5. Membuat build Android Automotive OS

Meskipun aplikasi "berfungsi", ada beberapa perubahan kecil yang perlu dilakukan agar aplikasi berfungsi dengan baik di Android Automotive OS dan memenuhi persyaratan agar dapat dipublikasikan di Play Store. Tidak semua perubahan ini masuk akal untuk disertakan dalam aplikasi versi seluler, jadi Anda akan membuat varian build Android Automotive OS terlebih dahulu.

Menambahkan dimensi ragam faktor bentuk

Untuk memulai, tambahkan dimensi ragam untuk faktor bentuk yang ditargetkan oleh build dengan mengubah flavorDimensions dalam file build.gradle.kts. Kemudian, tambahkan blok productFlavors dan ragam untuk setiap faktor bentuk (mobile dan automotive).

Untuk informasi selengkapnya, lihat Mengonfigurasi ragam produk.

build.gradle.kts (Module :app)

android {
    ...
    flavorDimensions += "formFactor"
    productFlavors {
        create("mobile") {
            // Inform Android Studio to use this flavor as the default (e.g. in the Build Variants tool window)
            isDefault = true
            // Since there is only one flavor dimension, this is optional
            dimension = "formFactor"
        }
        create("automotive") {
            // Since there is only one flavor dimension, this is optional
            dimension = "formFactor"
            // Adding a suffix makes it easier to differentiate builds (e.g. in the Play Console)
            versionNameSuffix = "-automotive"
        }
    }
    ...
}

Setelah memperbarui file build.gradle.kts, Anda akan melihat banner di bagian atas file yang memberitahukan bahwa "Gradle files have changed since the last project sync. A project sync may be necessary for the IDE to work properly". Klik tombol Sync Now di banner tersebut sehingga Android Studio dapat mengimpor perubahan konfigurasi build ini.

8685bcde6b21901f.png

Selanjutnya, buka jendela alat Build Variants dari item menu Build > Select Build Variant... dan pilih varian automotiveDebug. Tindakan ini akan memastikan Anda melihat file untuk set sumber automotive di jendela Project dan varian build ini digunakan saat menjalankan aplikasi melalui Android Studio.

19e4aa8135553f62.png

Membuat manifes Android Automotive OS

Selanjutnya, Anda akan membuat file AndroidManifest.xml untuk set sumber automotive. File ini berisi elemen yang diperlukan dari aplikasi Android Automotive OS.

  1. Di jendela Project, klik kanan modul app. Dari menu dropdown yang muncul, pilih New > Other > Android Manifest File
  2. Di jendela New Android Component yang terbuka, pilih automotive sebagai Target Source Set untuk file baru. Klik Finish untuk membuat file.

3fe290685a1026f5.png

  1. Dalam file AndroidManifest.xml yang baru saja dibuat (di jalur app/src/automotive/AndroidManifest.xml), tambahkan kode berikut:

AndroidManifest.xml (automotive)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!--  https://developer.android.com/training/cars/parked#required-features  -->
    <uses-feature
        android:name="android.hardware.type.automotive"
        android:required="true" />
    <uses-feature
        android:name="android.hardware.wifi"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.screen.portrait"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.screen.landscape"
        android:required="false" />
</manifest>

Deklarasi pertama diperlukan untuk mengupload artefak build ke jalur Android Automotive OS di Konsol Play. Dengan adanya fitur ini, Google Play dapat mendistribusikan aplikasi hanya ke perangkat yang memiliki fitur android.hardware.type.automotive (yaitu mobil).

Deklarasi lainnya diperlukan untuk memastikan aplikasi dapat diinstal di berbagai konfigurasi hardware yang ada di mobil. Untuk detail selengkapnya, lihat Fitur Android Automotive OS yang diperlukan.

Menandai aplikasi sebagai aplikasi video

Bagian terakhir metadata yang perlu ditambahkan adalah file automotive_app_desc.xml. File ini digunakan untuk mendeklarasikan kategori aplikasi Anda dalam konteks Android untuk Mobil, dan tidak bergantung pada kategori yang Anda pilih untuk aplikasi di Konsol Play.

  1. Klik kanan modul app dan pilih opsi New > Android Resource File, lalu masukkan nilai berikut sebelum mengklik OK:
  • Nama file: automotive_app_desc.xml
  • Jenis resource: XML
  • Elemen root: automotiveApp
  • Set sumber: automotive
  • Nama direktori: xml

47ac6bf76ef8ad45.png

  1. Dalam file tersebut, tambahkan elemen <uses> berikut untuk mendeklarasikan bahwa aplikasi Anda adalah aplikasi video.

automotive_app_desc.xml

<?xml version="1.0" encoding="utf-8"?>
<automotiveApp xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses
        name="video"
        tools:ignore="InvalidUsesTagAttribute" />
</automotiveApp>
  1. Di file AndroidManifest.xml set sumber automotive (tempat Anda baru saja menambahkan elemen <uses-feature>), tambahkan elemen <application> kosong. Di dalamnya, tambahkan elemen <meta-data> berikut yang merujuk ke file automotive_app_desc.xml yang baru saja Anda buat.

AndroidManifest.xml (automotive)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...

    <application>
        <meta-data
            android:name="com.android.automotive"
            android:resource="@xml/automotive_app_desc" />
    </application>
</manifest>

Dengan demikian, Anda telah membuat semua perubahan yang diperlukan untuk membuat build Android Automotive OS aplikasi.

6. Memenuhi persyaratan kualitas Android Automotive OS: Kemampuan Navigasi

Meskipun membuat varian build Android Automotive OS adalah salah satu bagian dari menghadirkan aplikasi Anda di mobil, Anda tetap perlu memastikan aplikasi dapat digunakan dan aman digunakan.

Menambahkan kemampuan navigasi

Saat menjalankan aplikasi di emulator Android Automotive OS, Anda mungkin melihat bahwa kembali dari layar detail ke layar utama atau dari layar pemutar ke layar detail tidak mungkin dilakukan. Tidak seperti faktor bentuk lainnya, yang mungkin memerlukan tombol kembali atau gestur sentuh untuk mengaktifkan navigasi kembali, perangkat Android Automotive OS tidak memiliki persyaratan semacam itu. Dengan demikian, aplikasi harus menyediakan kemampuan navigasi di UI-nya untuk memastikan pengguna dapat melakukan navigasi tanpa terhenti di layar dalam aplikasi. Persyaratan ini dikodifikasi sebagai pedoman kualitas AN-1.

Untuk mendukung navigasi kembali dari layar detail ke layar utama, tambahkan parameter navigationIcon tambahan untuk CenterAlignedTopAppBar layar detail sebagai berikut:

RoadReelsApp.kt

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton

...

navigationIcon = {
    IconButton(onClick = { navController.popBackStack() }) {
        Icon(
            Icons.AutoMirrored.Filled.ArrowBack,
            contentDescription = null
        )
    }
}

Untuk mendukung navigasi kembali dari layar pemutar ke layar utama:

  1. Perbarui composable TopControls untuk mengambil parameter callback bernama onClose dan tambahkan IconButton yang memanggilnya saat diklik.

PlayerControls.kt

@Composable
fun TopControls(
    title: String?,
    onClose: () -> Unit,
    modifier: Modifier = Modifier
) {
    Box(modifier) {
        IconButton(
            modifier = Modifier
                .align(Alignment.TopStart),
            onClick = onClose
        ) {
            Icon(
                Icons.TwoTone.Close,
                contentDescription = "Close player",
                tint = Color.White
            )
        }

        if (title != null) { ... }
    }
}
  1. Perbarui composable PlayerControls untuk mengambil juga parameter callback onClose dan meneruskannya ke TopControls

PlayerControls.kt

fun PlayerControls(
    visible: Boolean,
    playerState: PlayerState,
    onClose: () -> Unit,
    onPlayPause: () -> Unit,
    onSeek: (seekToMillis: Long) -> Unit,
    modifier: Modifier = Modifier,
) {
    AnimatedVisibility(
        visible = visible,
        enter = fadeIn(),
        exit = fadeOut()
    ) {
        Box(modifier = modifier.background(Color.Black.copy(alpha = .5f))) {
            TopControls(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(dimensionResource(R.dimen.screen_edge_padding))
                    .align(Alignment.TopCenter),
                title = playerState.mediaMetadata.title?.toString(),
                onClose = onClose
            )
            ...
        }
    }
}
  1. Selanjutnya, perbarui composable PlayerScreen untuk mengambil parameter yang sama, dan teruskan ke PlayerControls-nya.

PlayerScreen.kt

@Composable
fun PlayerScreen(
    onClose: () -> Unit,
    modifier: Modifier = Modifier,
) {
    ...

    PlayerControls(
        modifier = Modifier
            .fillMaxSize(),
        visible = isShowingControls,
        playerState = playerState,
        onClose = onClose,
        onPlayPause = { if (playerState.isPlaying) player.pause() else player.play() },
        onSeek = { player.seekTo(it) }
    )
}
  1. Terakhir, di RoadReelsNavHost, berikan implementasi yang akan diteruskan ke PlayerScreen:

RoadReelsNavHost.kt

composable(route = Screen.Player.name) {
    PlayerScreen(onClose = { navController.popBackStack() })
}

Bagus, sekarang pengguna dapat berpindah antarlayar tanpa terhenti. Selain itu, pengalaman pengguna mungkin juga lebih baik pada faktor bentuk lainnya – misalnya, pada ponsel yang tinggi ketika tangan pengguna sudah berada di dekat bagian atas layar, mereka dapat dengan lebih mudah menavigasi aplikasi tanpa perlu memindahkan perangkat di tangan mereka.

43122e716eeeeb20.gif

Menyesuaikan dengan dukungan orientasi layar

Tidak seperti kebanyakan perangkat seluler, sebagian besar mobil memiliki orientasi tetap. Artinya, mobil tersebut mendukung orientasi lanskap atau potret, tetapi tidak keduanya, karena layarnya tidak dapat diputar. Oleh karena itu, aplikasi harus menghindari asumsi bahwa kedua orientasi didukung.

Dalam Create an Android Automotive OS manifest, Anda menambahkan dua elemen <uses-feature> untuk fitur android.hardware.screen.portrait dan android.hardware.screen.landscape dengan atribut required yang ditetapkan ke false. Tindakan tersebut memastikan bahwa tidak ada dependensi fitur yang implisit pada salah satu orientasi layar yang dapat mencegah aplikasi didistribusikan ke mobil. Akan tetapi, elemen manifes tersebut tidak mengubah perilaku aplikasi, hanya cara pendistribusiannya.

Saat ini, aplikasi memiliki fitur berguna yang otomatis menetapkan orientasi aktivitas ke lanskap saat pemutar video terbuka, sehingga pengguna ponsel tidak perlu mengutak-atik perangkat mereka untuk mengubah orientasinya jika belum berupa lanskap.

Sayangnya, perilaku yang sama dapat menyebabkan loop berkedip atau tampilan lebar di perangkat dengan orientasi potret tetap, seperti yang dimiliki banyak mobil laik jalan saat ini.

Untuk memperbaikinya, Anda dapat menambahkan pemeriksaan berdasarkan orientasi layar yang didukung perangkat saat ini.

  1. Untuk menyederhanakan implementasinya, tambahkan terlebih dahulu kode berikut di Extensions.kt:

Extensions.kt

import android.content.Context
import android.content.pm.PackageManager

...

enum class SupportedOrientation {
    Landscape,
    Portrait,
}

fun Context.supportedOrientations(): List<SupportedOrientation> {
    return when (Pair(
        packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_LANDSCAPE),
        packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_PORTRAIT)
    )) {
        Pair(true, false) -> listOf(SupportedOrientation.Landscape)
        Pair(false, true) -> listOf(SupportedOrientation.Portrait)
        // For backwards compat, if neither feature is declared, both can be assumed to be supported
        else -> listOf(SupportedOrientation.Landscape, SupportedOrientation.Portrait)
    }
}
  1. Kemudian, simpan panggilan untuk menetapkan orientasi yang diminta. Karena aplikasi dapat mengalami masalah serupa dalam mode multi-aplikasi di perangkat seluler, Anda juga dapat menyertakan pemeriksaan agar tidak menyetel orientasi secara dinamis dalam kasus tersebut.

PlayerScreen.kt

import com.example.android.cars.roadreels.SupportedOrientation
import com.example.android.cars.roadreels.supportedOrientations

...

LaunchedEffect(Unit) {
    ...

    // Only automatically set the orientation to landscape if the device supports landscape.
    // On devices that are portrait only, the activity may enter a compat mode and won't get to
    // use the full window available if so. The same applies if the app's window is portrait
    // in multi-window mode.
    if (context.supportedOrientations().contains(SupportedOrientation.Landscape)
        && !context.isInMultiWindowMode
    ) {
        context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
    }

    ...
}

Layar pemutar memasuki loop berkedip pada emulator Polestar 2 sebelum menambahkan pemeriksaan (saat aktivitas tidak menangani perubahan konfigurasi orientasi)

Layar pemutar memiliki tampilan lebar pada emulator Polestar 2 sebelum menambahkan pemeriksaan (saat aktivitas menangani perubahan konfigurasi orientasi)

Layar pemutar tidak memiliki tampilan lebar pada emulator Polestar 2 setelah menambahkan pemeriksaan.

Layar pemutar memasuki loop berkedip pada emulator Polestar 2 sebelum menambahkan pemeriksaan (saat aktivitas tidak menangani perubahan konfigurasi orientation)

Layar pemutar memiliki tampilan lebar pada emulator Polestar 2 sebelum menambahkan pemeriksaan (saat aktivitas menangani perubahan konfigurasi orientation)

Layar pemutar tidak memiliki tampilan lebar pada emulator Polestar 2 setelah menambahkan pemeriksaan

Karena tempat ini adalah satu-satunya lokasi di aplikasi yang menetapkan orientasi layar, aplikasi kini menghindari tampilan lebar. Di aplikasi Anda sendiri, periksa apakah ada atribut screenOrientation atau panggilan setRequestedOrientation yang hanya ditujukan untuk orientasi lanskap atau potret (termasuk sensor, fitur balik, dan varian pengguna), lalu hapus atau simpan jika diperlukan untuk membatasi tampilan lebar. Untuk mengetahui detail selengkapnya, lihat Mode kompatibilitas perangkat.

Menyesuaikan dengan kemampuan kontrol kolom sistem

Sayangnya, meskipun perubahan sebelumnya memastikan aplikasi tidak memasuki loop berkedip atau membuat tampilan lebar, perubahan ini juga mengekspos asumsi lain yang rusak – yaitu bahwa kolom sistem selalu dapat disembunyikan. Karena pengguna memiliki kebutuhan yang berbeda saat menggunakan mobil (dibandingkan saat menggunakan ponsel atau tablet), OEM memiliki opsi untuk mencegah aplikasi menyembunyikan kolom sistem guna memastikan bahwa kontrol kendaraan, seperti pengontrol kondisi udara, selalu dapat diakses di layar.

Akibatnya, ada potensi bagi aplikasi untuk merender di belakang kolom sistem saat aplikasi tersebut merender dalam mode imersif dan menganggap bahwa kolom tersebut dapat disembunyikan. Anda dapat melihatnya di langkah sebelumnya, karena kontrol pemutar atas dan bawah tidak lagi terlihat saat aplikasi tidak dijadikan tampilan lebar. Dalam contoh khusus ini, aplikasi tidak lagi dapat dijelajahi karena tombol untuk menutup pemutar terhalang dan fungsinya terhambat karena bilah geser tidak dapat digunakan.

Perbaikan yang termudah adalah menerapkan padding inset jendela systemBars ke pemutar seperti berikut:

PlayerScreen.kt

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding

...

Box(
    modifier = Modifier
        .fillMaxSize()
        .windowInsetsPadding(WindowInsets.systemBars)
) {
    PlayerView(...)
    PlayerControls(...)
}

Namun, solusi ini tidak ideal karena menyebabkan elemen UI berpindah-pindah ketika kolom sistem bergerak.

9c51956e2093820a.gif

Untuk meningkatkan pengalaman pengguna, Anda dapat mengupdate aplikasi guna melacak inset mana yang dapat dikontrol dan menerapkan padding hanya untuk inset yang tidak dapat dikontrol.

  1. Karena layar lain dalam aplikasi mungkin ingin mengontrol inset jendela, sebaiknya teruskan inset yang dapat dikontrol sebagai CompositionLocal. Buat file baru, LocalControllableInsets.kt, di paket com.example.android.cars.roadreels dan tambahkan kode berikut:

LocalControllableInsets.kt

import androidx.compose.runtime.compositionLocalOf

// Assume that no insets can be controlled by default
const val DEFAULT_CONTROLLABLE_INSETS = 0
val LocalControllableInsets = compositionLocalOf { DEFAULT_CONTROLLABLE_INSETS }
  1. Siapkan OnControllableInsetsChangedListener untuk memproses perubahan.

MainActivity.kt

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat.OnControllableInsetsChangedListener

...

class MainActivity : ComponentActivity() {
    private lateinit var onControllableInsetsChangedListener: OnControllableInsetsChangedListener

    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        enableEdgeToEdge()

        setContent {
            var controllableInsetsTypeMask by remember { mutableIntStateOf(DEFAULT_CONTROLLABLE_INSETS) }

            onControllableInsetsChangedListener =
                OnControllableInsetsChangedListener { _, typeMask ->
                    if (controllableInsetsTypeMask != typeMask) {
                        controllableInsetsTypeMask = typeMask
                    }
                }

            WindowCompat.getInsetsController(window, window.decorView)
                .addOnControllableInsetsChangedListener(onControllableInsetsChangedListener)

            RoadReelsTheme {
                RoadReelsApp(calculateWindowSizeClass(this))
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()

        WindowCompat.getInsetsController(window, window.decorView)
            .removeOnControllableInsetsChangedListener(onControllableInsetsChangedListener)
    }
}
  1. Tambahkan CompositionLocalProvider tingkat teratas yang berisi composable tema dan aplikasi, juga yang mengikat nilai ke LocalControllableInsets.

MainActivity.kt

import androidx.compose.runtime.CompositionLocalProvider

...

CompositionLocalProvider(LocalControllableInsets provides controllableInsetsTypeMask) {
    RoadReelsTheme {
        RoadReelsApp(calculateWindowSizeClass(this))
    }
}
  1. Pada pemutar, baca nilai saat ini dan gunakan untuk menentukan inset yang akan digunakan untuk padding.

PlayerScreen.kt

import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.union
import androidx.compose.ui.unit.dp
import com.example.android.cars.roadreels.LocalControllableInsets

...

val controllableInsetsTypeMask = LocalControllableInsets.current

// When the system bars can be hidden, ignore them when applying padding to the player and
// controls so they don't jump around as the system bars disappear. If they can't be hidden
// include them so nothing renders behind the system bars
var windowInsetsForPadding = WindowInsets(0.dp)
if (controllableInsetsTypeMask.and(WindowInsetsCompat.Type.statusBars()) == 0) {
    windowInsetsForPadding = windowInsetsForPadding.union(WindowInsets.statusBars)
}
if (controllableInsetsTypeMask.and(WindowInsetsCompat.Type.navigationBars()) == 0) {
    windowInsetsForPadding = windowInsetsForPadding.union(WindowInsets.navigationBars)
}

Box(
    modifier = Modifier
        .fillMaxSize()
        .windowInsetsPadding(windowInsetsForPadding)
) {
    PlayerView(...)
    PlayerControls(...)
}

Konten tidak berpindah-pindah ketika kolom sistem dapat disembunyikan

Konten tetap terlihat ketika kolom sistem tidak dapat disembunyikan

Konten tidak berpindah-pindah ketika kolom sistem dapat disembunyikan

Konten tetap terlihat ketika kolom sistem tidak dapat disembunyikan

Jauh lebih baik! Konten tidak akan berpindah-pindah, dan pada saat yang sama, kontrol sepenuhnya terlihat, bahkan di mobil yang kolom sistemnya tidak dapat dikontrol.

7. Memenuhi persyaratan kualitas Android Automotive OS: Gangguan bagi pengemudi

Terakhir, ada satu perbedaan utama antara mobil dan faktor bentuk lainnya, yaitu bahwa mobil digunakan untuk mengemudi. Oleh karena itu, membatasi gangguan saat berkendara sangatlah penting. Semua aplikasi parkir untuk Android Automotive OS harus menjeda pemutaran ketika mulai mengemudi. Overlay sistem muncul ketika mulai mengemudi, dan pada gilirannya, peristiwa siklus proses onPause dipanggil untuk aplikasi yang ditempatkan. Selama panggilan ini, aplikasi harus menjeda pemutaran.

Simulasi mengemudi

Buka tampilan pemutar di emulator dan mulai memutar konten. Kemudian, ikuti langkah-langkah untuk simulasi mengemudi dan perhatikan bahwa saat UI aplikasi terhalang oleh sistem, pemutaran tidak dijeda. Hal ini melanggar pedoman kualitas aplikasi mobil DD-2.

839af1382c1f10ca.png

Menjeda pemutaran ketika mulai mengemudi

  1. Tambahkan dependensi pada artefak androidx.lifecycle:lifecycle-runtime-compose, yang berisi LifecycleEventEffect yang membantu menjalankan kode pada peristiwa siklus proses.

libs.version.toml

androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }

Build.gradle.kts (Module :app)

implementation(libs.androidx.lifecycle.runtime.compose)
  1. Setelah menyinkronkan project untuk mendownload dependensi, tambahkan LifecycleEventEffect yang berjalan di peristiwa ON_PAUSE untuk menjeda pemutaran.

PlayerScreen.kt

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect

...

@Composable
fun PlayerScreen(...) {
    ...
    LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) {
        player.pause()
    }

    LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
        player.play()
    }
    ...
}

Setelah perbaikan diterapkan, ikuti langkah yang sama seperti yang Anda lakukan sebelumnya untuk simulasi mengemudi selama pemutaran aktif, dan perhatikan bahwa pemutaran berhenti, yang memenuhi persyaratan DD-2.

8. Menguji aplikasi di emulator distant display

Konfigurasi baru yang mulai muncul di mobil adalah penyiapan dua layar dengan layar utama di konsol tengah dan layar sekunder tinggi di dasbor, dekat kaca depan. Aplikasi dapat dipindahkan dari layar tengah ke layar sekunder, lalu kembali lagi untuk memberikan lebih banyak opsi kepada pengemudi dan penumpang.

Menginstal image Automotive Distant Display

  1. Pertama, buka SDK Manager di Android Studio, lalu pilih tab SDK Platforms jika belum dipilih. Di pojok kanan bawah jendela SDK Manager, pastikan kotak Show package details dicentang.
  2. Instal image emulator Automotive Distant Display with Google API untuk arsitektur komputer Anda (x86/ARM).

Membuat Perangkat Virtual Android untuk Android Automotive OS

  1. Setelah membuka Pengelola Perangkat, pilih Automotive di kolom Category di sisi kiri jendela. Kemudian, pilih profil hardware paket Automotive Distant Display dari daftar, lalu klik Next.
  2. Di halaman berikutnya, pilih image sistem dari langkah sebelumnya. Klik Next dan pilih opsi lanjutan yang Anda inginkan sebelum akhirnya membuat AVD dengan mengklik Finish.

Menjalankan aplikasi

Jalankan aplikasi di emulator yang baru saja Anda buat menggunakan konfigurasi run app yang ada. Ikuti petunjuk di Menggunakan emulator distant display untuk memindahkan aplikasi ke dan dari distant display. Uji pemindahan aplikasi saat berada di layar utama/detail dan saat berada di layar pemutar, serta saat mencoba berinteraksi dengan aplikasi di kedua layar.

b277bd18a94e9c1b.png

9. Meningkatkan pengalaman aplikasi di distant display

Saat menggunakan aplikasi di distant display, Anda mungkin memperhatikan dua hal:

  1. Pemutaran dimulai ulang saat aplikasi dipindahkan ke dan dari distant display.
  2. Anda tidak dapat berinteraksi dengan aplikasi saat berada di distant display, termasuk mengubah status pemutaran.

Meningkatkan kontinuitas aplikasi

Masalah saat pemutaran dimulai ulang disebabkan oleh aktivitas yang dibuat ulang karena perubahan konfigurasi. Karena aplikasi ditulis menggunakan Compose dan konfigurasi yang berubah terkait dengan ukuran, Compose dapat menangani perubahan konfigurasi untuk Anda dengan membatasi pembuatan ulang aktivitas untuk perubahan konfigurasi berbasis ukuran. Hal ini membuat transisi antarlayar berjalan lancar, tanpa penghentian dalam pemutaran atau pemuatan ulang karena pembuatan ulang aktivitas.

AndroidManifest.xml

<activity
    android:name="com.example.android.cars.roadreels.MainActivity"
    ...
    android:configChanges="screenSize|smallestScreenSize|orientation|screenLayout|density">
        ...
</activity>

Menerapkan kontrol pemutaran

Untuk memperbaiki masalah ketika aplikasi tidak dapat dikontrol saat berada di distant display, Anda dapat menerapkan MediaSession. Sesi media menyediakan cara universal untuk berinteraksi dengan pemutar audio atau video. Untuk informasi selengkapnya, lihat Mengontrol dan memberitahukan pemutaran menggunakan MediaSession.

  1. Menambahkan dependensi pada artefak androidx.media3:media3-session

libs.version.toml

androidx-media3-mediasession = { group = "androidx.media3", name = "media3-session", version.ref = "media3" }

build.gradle.kts (Module :app)

implementation(libs.androidx.media3.mediasession)
  1. Buat MediaSession menggunakan builder-nya.

PlayerScreen.kt

import androidx.media3.session.MediaSession

@Composable
fun PlayerScreen(...) {
    ...
    val mediaSession = remember(context, player) {
        MediaSession.Builder(context, player).build()
    }
    ...
}
  1. Kemudian, tambahkan baris tambahan di blok onDispose dari DisposableEffect di composable Player untuk merilis MediaSession saat Player keluar dari hierarki komposisi.

PlayerScreen.kt

DisposableEffect(Unit) {
    onDispose {
        mediaSession.release()
        player.release()
        ...
    }
}
  1. Terakhir, saat berada di layar pemutar, Anda dapat menguji kontrol media menggunakan perintah adb shell cmd media_session dispatch
# To play content
adb shell cmd media_session dispatch play

# To pause content
adb shell cmd media_session dispatch pause

# To toggle the playing state
adb shell cmd media_session dispatch play-pause

Dengan demikian, aplikasi ini berfungsi jauh lebih baik di mobil dengan distant display! Namun lebih dari itu, aplikasi juga berfungsi lebih baik pada faktor bentuk lainnya. Pada perangkat yang dapat memutar layar atau memungkinkan pengguna mengubah ukuran jendela aplikasi, aplikasi kini juga beradaptasi dengan lancar dalam situasi tersebut.

Selain itu, berkat integrasi sesi media, pemutaran aplikasi dapat dikontrol tidak hanya oleh kontrol hardware dan software di mobil, tetapi juga oleh sumber lain, seperti kueri Asisten Google atau tombol jeda pada perangkat headphone, sehingga pengguna memiliki lebih banyak opsi untuk mengontrol aplikasi di berbagai faktor bentuk.

10. Menguji aplikasi dengan konfigurasi sistem yang berbeda

Karena aplikasi berfungsi dengan baik di layar utama dan distant display, hal terakhir yang harus diperiksa adalah cara aplikasi menangani konfigurasi kolom sistem dan potongan layar yang berbeda. Seperti dijelaskan dalam Menggunakan inset jendela dan potongan layar, perangkat Android Automotive OS mungkin memiliki konfigurasi yang merusak asumsi yang umumnya berlaku pada faktor bentuk seluler.

Di bagian ini, Anda akan mendownload emulator yang dapat dikonfigurasi saat runtime, mengonfigurasi emulator agar memiliki kolom sistem sebelah kiri, dan menguji aplikasi dalam konfigurasi tersebut.

Menginstal image Android Automotive dengan Google API

  1. Pertama, buka SDK Manager di Android Studio, lalu pilih tab SDK Platforms jika belum dipilih. Di pojok kanan bawah jendela SDK Manager, pastikan kotak Show package details dicentang.
  2. Instal image emulator Android Automotive dengan Google API menggunakan API 33 untuk arsitektur komputer Anda (x86/ARM).

Membuat Perangkat Virtual Android untuk Android Automotive OS

  1. Setelah membuka Pengelola Perangkat, pilih Automotive di kolom Category di sisi kiri jendela. Kemudian, pilih profil hardware gabungan Automotive (1080p landscape) dari daftar, lalu klik Next.
  2. Di halaman berikutnya, pilih image sistem dari langkah sebelumnya. Klik Next dan pilih opsi lanjutan yang Anda inginkan sebelum akhirnya membuat AVD dengan mengklik Finish.

Mengonfigurasi kolom sistem samping

Seperti dijelaskan dalam Pengujian menggunakan emulator yang dapat dikonfigurasi, ada berbagai opsi untuk mengemulasi berbagai konfigurasi sistem yang ada di mobil.

Untuk tujuan codelab ini, com.android.systemui.rro.left dapat digunakan untuk menguji berbagai konfigurasi kolom sistem. Untuk mengaktifkan kode tersebut, gunakan perintah berikut:

adb shell cmd overlay enable --user 0 com.android.systemui.rro.left

b642703a7278b219.png

Karena aplikasi menggunakan pengubah systemBars sebagai contentWindowInsets dalam Scaffold, konten sudah digambar di area yang aman di kolom sistem. Untuk melihat apa yang akan terjadi jika aplikasi mengasumsikan bahwa kolom sistem hanya muncul di bagian atas dan bawah layar, ubah parameter tersebut menjadi berikut:

RoadReelsApp.kt

contentWindowInsets = if (route?.equals(Screen.Player.name) == true) WindowInsets(0.dp) else WindowInsets.systemBars.only(WindowInsetsSides.Vertical)

Maaf. Layar daftar dan detail dirender di belakang kolom sistem. Berkat pekerjaan sebelumnya, layar pemutar akan berfungsi dengan baik, meskipun kolom sistem tidak dapat dikontrol setelahnya.

9898f7298a7dfb4.gif

Sebelum melanjutkan ke bagian berikutnya, pastikan untuk mengembalikan perubahan yang baru saja Anda buat pada parameter windowContentPadding.

11. Menggunakan potongan layar

Terakhir, beberapa mobil memiliki layar dengan potongan layar yang sangat berbeda jika dibandingkan dengan yang terlihat di perangkat seluler. Sebagai ganti potongan kamera pinhole atau notch, beberapa kendaraan Android Automotive OS memiliki layar melengkung yang membuat layar menjadi non-persegi panjang.

Untuk melihat bagaimana perilaku aplikasi ketika potongan layar seperti itu ada, pertama-tama aktifkan potongan layar menggunakan perintah berikut:

adb shell cmd overlay enable --user 0 com.android.internal.display.cutout.emulation.free_form

Untuk benar-benar menguji seberapa baik perilaku aplikasi, aktifkan juga kolom sistem sebelah kiri yang digunakan di bagian terakhir, jika belum diaktifkan:

adb shell cmd overlay enable --user 0 com.android.systemui.rro.left

Sama halnya, aplikasi tidak merender ke potongan layar (bentuk persis potongan tersebut sulit diketahui saat ini, tetapi akan menjadi jelas di langkah berikutnya). Hal ini tidak menjadi masalah dan memberikan pengalaman yang lebih baik daripada aplikasi yang merender ke potongan, tetapi tidak beradaptasi dengan hati-hati.

212628db84981025.gif

Merender ke potongan layar

Untuk memberi pengalaman yang paling imersif kepada pengguna, Anda dapat memanfaatkan ruang layar yang lebih luas dengan merender ke potongan layar.

  1. Untuk merender ke potongan layar, buat file integers.xml guna menyimpan penggantian khusus untuk mobil. Untuk melakukannya, gunakan penentu mode UI dengan nilai Dok Mobil (nama ini adalah peninggalan dari saat hanya Android Auto yang ada, tetapi kini juga digunakan Android Automotive OS). Selain itu, karena LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS diperkenalkan di Android R, tambahkan juga penentu Versi Android dengan nilai 30. Lihat Menggunakan resource alternatif untuk detail selengkapnya.

22b7f17657cac3fd.png

  1. Dalam file yang baru saja Anda buat (res/values-car-v30/integers.xml), tambahkan kode berikut:

integers.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="windowLayoutInDisplayCutoutMode">3</integer>
</resources>

Nilai bilangan bulat 3 sesuai dengan LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS dan menggantikan nilai default 0 dari res/values/integers.xml, yang sesuai dengan LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT. Nilai bilangan bulat ini sudah direferensikan di MainActivity.kt untuk mengganti mode yang ditetapkan oleh enableEdgeToEdge(). Untuk mengetahui informasi selengkapnya tentang atribut ini, lihat dokumentasi referensi.

Sekarang, saat Anda menjalankan aplikasi, perhatikan bahwa konten meluas ke potongan dan terlihat sangat imersif. Namun, panel aplikasi atas dan beberapa konten terhalang sebagian oleh potongan layar, menyebabkan masalah yang mirip dengan yang terjadi saat aplikasi menganggap kolom sistem hanya akan muncul di bagian atas dan bawah.

f0eefa42dee6f7c7.gif

Memperbaiki panel aplikasi atas

Untuk memperbaiki panel aplikasi atas, Anda dapat menambahkan parameter windowInsets berikut ke Composable CenterAlignedTopAppBar:

RoadReelsApp.kt

import androidx.compose.foundation.layout.safeDrawing

...

windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)

Karena safeDrawing terdiri dari inset displayCutout dan systemBars, hal ini meningkatkan parameter windowInsets default, yang hanya menggunakan systemBars saat memosisikan panel aplikasi atas.

Selain itu, karena panel aplikasi atas diposisikan di bagian atas jendela, Anda tidak boleh menyertakan komponen bawah inset safeDrawing karena hal ini berpotensi menambahkan padding yang tidak perlu.

7d59ebb63ada5f71.gif

Memperbaiki layar utama

Salah satu opsi untuk memperbaiki konten di layar utama dan layar detail adalah menggunakan safeDrawing, bukan systemBars untuk contentWindowInsets dari Scaffold. Namun, aplikasi terlihat kurang imersif menggunakan opsi tersebut, dengan konten yang tiba-tiba terpotong saat potongan layar dimulai – tidak lebih baik dibandingkan jika aplikasi tidak dirender ke potongan layar sama sekali.

6b3824ca3214cbfa.gif

Untuk antarmuka pengguna yang lebih imersif, Anda dapat menangani inset pada setiap komponen dalam layar.

  1. Perbarui contentWindowInsets dari Scaffold agar selalu menjadi 0 dp (bukan hanya untuk PlayerScreen). Hal ini memungkinkan setiap layar dan/atau komponen dalam layar menentukan perilakunya terkait inset.

RoadReelsApp.kt

Scaffold(
    ...,
    contentWindowInsets = WindowInsets(0.dp)
) { ... }
  1. Setel windowInsetsPadding dari composable Text header baris untuk menggunakan komponen horizontal inset safeDrawing. Komponen atas inset ini ditangani oleh panel aplikasi atas, dan komponen bawah akan ditangani nanti.

MainScreen.kt

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding

...

LazyColumn(
    contentPadding = PaddingValues(bottom = dimensionResource(R.dimen.screen_edge_padding))
) {
    items(NUM_ROWS) { rowIndex: Int ->
        Text(
            "Row $rowIndex",
            style = MaterialTheme.typography.headlineSmall,
            modifier = Modifier
                .padding(
                    horizontal = dimensionResource(R.dimen.screen_edge_padding),
                    vertical = dimensionResource(R.dimen.row_header_vertical_padding)
                )
                .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal))
        )
    ...
}
  1. Hapus parameter contentPadding dari LazyRow. Kemudian, di awal dan akhir setiap LazyRow, tambahkan Spacer selebar komponen safeDrawing yang sesuai untuk memastikan semua thumbnail dapat dilihat sepenuhnya. Gunakan pengubah widthIn untuk memastikan pengatur jarak ini setidaknya selebar padding konten. Tanpa elemen ini, item di awal dan akhir baris mungkin akan terhalang di belakang kolom sistem dan/atau potongan layar, bahkan saat digeser sepenuhnya ke awal/akhir baris.

MainScreen.kt

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsEndWidth
import androidx.compose.foundation.layout.windowInsetsStartWidth

...

LazyRow(
    horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.list_item_spacing)),
) {
    item {
        Spacer(
            Modifier
                .windowInsetsStartWidth(WindowInsets.safeDrawing)
                .widthIn(min = dimensionResource(R.dimen.screen_edge_padding))
        )
    }
    items(NUM_ITEMS_PER_ROW) { ... }
    item {
        Spacer(
            Modifier
                .windowInsetsEndWidth(WindowInsets.safeDrawing)
                .widthIn(min = dimensionResource(R.dimen.screen_edge_padding))
        )
    }
}
  1. Terakhir, tambahkan Spacer di akhir LazyColumn untuk memperhitungkan setiap kolom sistem atau inset potongan layar yang potensial di bagian bawah layar. Pengatur jarak yang setara di bagian atas LazyColumn tidak diperlukan karena panel aplikasi atas akan menanganinya. Jika aplikasi menggunakan panel aplikasi bawah, bukan panel aplikasi atas, Anda perlu menambahkan Spacer di awal daftar menggunakan pengubah windowInsetsTopHeight. Dan jika aplikasi menggunakan panel aplikasi atas dan bawah, tidak ada pengatur jarak yang diperlukan.

MainScreen.kt

import androidx.compose.foundation.layout.windowInsetsBottomHeight

...

LazyColumn(...){
    items(NUM_ROWS) { ... }
    item {
        Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
    }
}

Sempurna, panel aplikasi atas sepenuhnya terlihat dan, saat men-scroll ke akhir baris, Anda kini dapat melihat semua thumbnail secara keseluruhan.

543706473398114a.gif

Memperbaiki layar detail

f622958a8d0c16c8.png

Layar detail tidak terlalu buruk, tetapi konten masih terpotong.

Karena layar detail tidak memiliki konten yang dapat di-scroll, yang diperlukan untuk memperbaikinya adalah menambahkan pengubah windowInsetsPadding pada Box tingkat atas.

DetailScreen.kt

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding

...

Box(
    modifier = modifier
        .padding(dimensionResource(R.dimen.screen_edge_padding))
        .windowInsetsPadding(WindowInsets.safeDrawing)
) { ... }

bdd6de6010fc139d.png

Memperbaiki layar pemutar

Meskipun PlayerScreen sudah menerapkan padding untuk beberapa atau semua inset jendela kolom sistem di bagian Memenuhi persyaratan kualitas Android Automotive OS: Kemampuan Navigasi, hal itu tidak cukup untuk memastikan bahwa sekarang layar pemutar tidak tertutup sehingga aplikasi dirender ke potongan layar. Pada perangkat seluler, potongan layar hampir selalu sepenuhnya berada dalam kolom sistem. Namun, di mobil, potongan layar mungkin jauh melampaui kolom sistem, sehingga melanggar asumsi.

427227df5e44f554.png

Untuk memperbaikinya, cukup ubah nilai awal variabel windowInsetsForPadding dari nilai nol menjadi displayCutout:

PlayerScreen.kt

import androidx.compose.foundation.layout.displayCutout

...

var windowInsetsForPadding = WindowInsets(WindowInsets.displayCutout)

b523d8c1e1423757.gif

Bagus, aplikasi ini benar-benar mengoptimalkan layar sekaligus tetap dapat digunakan.

Selain itu, jika Anda menjalankan aplikasi di perangkat seluler, aplikasi tersebut juga akan lebih imersif. Item daftar dirender hingga ke tepi layar, termasuk di belakang menu navigasi.

dc7918499a33df31.png

12. Selamat

Anda berhasil memigrasikan dan mengoptimalkan aplikasi parkir pertama Anda. Sekarang saatnya menggunakan yang telah Anda pelajari dan menerapkannya ke aplikasi Anda sendiri.

Untuk dicoba

Bacaan lebih lanjut