Menavigasi antarlayar dengan Compose

1. Sebelum memulai

Hingga saat ini, aplikasi yang telah Anda kerjakan terdiri dari satu layar. Namun, banyak aplikasi yang Anda gunakan mungkin memiliki beberapa layar yang dapat dinavigasi. Misalnya, aplikasi Setelan memiliki banyak halaman konten yang tersebar di berbagai layar.

Dalam pengembangan Android modern, aplikasi multilayar dibuat menggunakan komponen Jetpack Navigation. Komponen Navigation Compose memungkinkan Anda membangun aplikasi multilayar di Compose dengan mudah menggunakan pendekatan deklaratif, seperti membangun antarmuka pengguna. Codelab ini memperkenalkan dasar-dasar komponen Navigation Compose, cara membuat AppBar responsif, dan cara mengirim data dari aplikasi Anda ke aplikasi lain menggunakan intent, sekaligus menunjukkan praktik terbaik dalam aplikasi yang makin kompleks.

Prasyarat

  • Pemahaman tentang bahasa Kotlin, termasuk jenis fungsi, lambda, dan fungsi cakupan
  • Pemahaman tentang tata letak Row dan Column dasar di Compose

Yang akan Anda pelajari

  • Membuat composable NavHost untuk menentukan rute dan layar di aplikasi Anda.
  • Beralih antarlayar menggunakan NavHostController.
  • Memanipulasi data sebelumnya untuk beralih ke layar sebelumnya.
  • Menggunakan intent untuk berbagi data dengan aplikasi lain.
  • Menyesuaikan AppBar, termasuk judul dan tombol kembali.

Yang akan Anda build

  • Anda akan mengimplementasikan navigasi di aplikasi multilayar.

Yang Anda butuhkan

  • Versi terbaru Android Studio
  • Koneksi internet untuk mendownload kode awal

2. 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-cupcake.git
$ cd basic-android-kotlin-compose-training-cupcake
$ git checkout starter

Jika Anda ingin melihat kode awal untuk codelab ini, lihat kode tersebut di GitHub.

3. Panduan aplikasi

Aplikasi Cupcake sedikit berbeda dari aplikasi yang telah Anda tangani sejauh ini. Aplikasi ini tidak menampilkan semua konten di satu layar, melainkan di empat layar terpisah. Selain itu, pengguna dapat menjelajahi setiap layar sambil memesan cupcake. Jika menjalankan aplikasi ini, Anda tidak akan melihat apa pun dan tidak akan dapat beralih antar-layar karena komponen navigasi belum ditambahkan ke kode aplikasi. Namun, Anda masih dapat memeriksa pratinjau composable untuk setiap layar dan mencocokkannya dengan layar akhir aplikasi di bawah.

Layar untuk memulai pesanan

Layar pertama menampilkan tiga tombol kepada pengguna yang sesuai dengan jumlah cupcake yang dapat dipesan.

Dalam kode, layar ini diwakili oleh composable StartOrderScreen di StartOrderScreen.kt.

Layar terdiri dari satu kolom, dengan gambar dan teks, beserta tiga tombol khusus untuk memesan cupcake dalam jumlah berbeda. Tombol kustom diimplementasikan oleh composable SelectQuantityButton, yang juga ada di StartOrderScreen.kt.

Layar untuk memilih rasa

Setelah memilih jumlah, aplikasi akan meminta pengguna untuk memilih rasa cupcake. Aplikasi menggunakan tombol pilihan untuk menampilkan berbagai opsi. Pengguna dapat memilih satu rasa dari beberapa pilihan kemungkinan rasa.

Daftar kemungkinan rasa disimpan sebagai daftar ID resource string di data.DataSource.kt.

Layar untuk memilih tanggal pengambilan

Setelah memilih rasa, aplikasi menampilkan serangkaian tombol pilihan lain kepada pengguna untuk memilih tanggal pengambilan. Opsi pengambilan berasal dari daftar yang ditampilkan oleh fungsi pickupOptions() di OrderViewModel.

Layar Pilih Rasa dan layar Pilih Tanggal Pengambilan diwakili oleh composable yang sama, SelectOptionScreen di SelectOptionScreen.kt. Mengapa menggunakan composable yang sama? Tata letak layar ini sama persis. Satu-satunya perbedaan adalah data, tetapi Anda dapat menggunakan composable yang sama untuk menampilkan layar untuk memilih rasa dan tanggal pengambilan.

Layar Ringkasan Pesanan

Setelah memilih tanggal pengambilan, aplikasi akan menampilkan layar Ringkasan Pesanan tempat pengguna dapat meninjau dan menyelesaikan pesanan.

Layar ini diimplementasikan oleh composable OrderSummaryScreen di SummaryScreen.kt.

Tata letak terdiri dari Column yang berisi semua informasi pesanan pengguna, composable Text untuk subtotal, dan tombol untuk mengirimkan pesanan ke aplikasi lain atau membatalkan pesanan dan kembali ke layar pertama.

Jika pengguna memilih untuk mengirim pesanan ke aplikasi lain, aplikasi Cupcake akan menampilkan Android ShareSheet yang menunjukkan opsi berbagi yang berbeda.

a32e016a6ccbf427.png

Status aplikasi saat ini disimpan di data.OrderUiState.kt. Class data OrderUiState berisi properti untuk menyimpan pilihan pengguna dari setiap layar.

Layar aplikasi akan ditampilkan dalam composable CupcakeApp. Namun, dalam project permulaan, aplikasi hanya menampilkan layar pertama. Saat ini, Anda tidak dapat membuka semua layar aplikasi, tetapi jangan khawatir, untuk itulah Anda ada di sini. Anda akan mempelajari cara menentukan rute navigasi, menyiapkan composable NavHost untuk melakukan navigasi antarlayar—yang juga dikenal sebagai tujuan—melakukan intent untuk berintegrasi dengan komponen UI sistem seperti layar berbagi, dan membuat AppBar merespons perubahan navigasi.

Composable yang dapat digunakan kembali

Jika sesuai, aplikasi contoh dalam kursus ini dirancang untuk menerapkan praktik terbaik. Begitu pula dengan aplikasi Cupcake. Dalam paket ui.components, Anda akan melihat file bernama CommonUi.kt yang berisi composable FormattedPriceLabel. Beberapa layar di aplikasi menggunakan composable ini untuk memformat harga pesanan secara konsisten. Daripada membuat duplikat composable Text yang sama dengan format dan pengubah yang sama, Anda dapat menentukan FormattedPriceLabel satu kali, lalu menggunakannya kembali sebanyak yang diperlukan untuk layar lainnya.

Layar rasa dan tanggal pengambilan menggunakan composable SelectOptionScreen, yang juga dapat digunakan kembali. Composable ini mengambil parameter bernama options dari jenis List<String> yang mewakili opsi untuk ditampilkan. Opsi muncul di Row, yang terdiri dari composable RadioButton dan composable Text yang berisi setiap string. Column mengelilingi seluruh tata letak dan juga berisi composable Text untuk menampilkan harga berformat, tombol Cancel, dan tombol Next.

4. Menentukan rute dan membuat NavHostController

Bagian dari Komponen Navigasi

Komponen Navigasi memiliki tiga bagian utama:

  • NavController: Bertanggung jawab untuk menavigasi di antara tujuan—yaitu layar di aplikasi Anda.
  • NavGraph: Memetakan tujuan composable untuk dinavigasi.
  • NavHost: Composable yang bertindak sebagai container untuk menampilkan tujuan NavGraph saat ini.

Dalam codelab ini, Anda akan berfokus pada NavController dan NavHost. Dalam NavHost, Anda akan menentukan tujuan untuk NavGraph aplikasi Cupcake.

Menentukan rute untuk tujuan di aplikasi Anda

Salah satu konsep dasar navigasi di aplikasi Compose adalah rute. Rute adalah string yang sesuai dengan tujuan. Ide ini mirip dengan konsep URL. Sama seperti URL berbeda yang dipetakan ke halaman yang berbeda di situs, rute adalah string yang dipetakan ke tujuan dan berfungsi sebagai ID uniknya. Tujuan biasanya berupa Composable tunggal atau grup Composable yang sesuai dengan apa yang dilihat pengguna. Aplikasi Cupcake memerlukan tujuan untuk layar mulai pesanan, layar rasa, layar tanggal pengambilan, dan layar ringkasan pesanan.

Aplikasi memiliki jumlah layar yang terbatas, sehingga rute juga terbatas. Anda dapat menentukan rute aplikasi menggunakan class enum. Class Enum di Kotlin memiliki properti nama yang menampilkan string dengan nama properti.

Anda akan mulai dengan menentukan empat rute aplikasi Cupcake.

  • Start: Pilih jumlah cupcake dari salah satu dari tiga tombol.
  • Flavor: Pilih rasa dari daftar pilihan.
  • Pickup: Pilih tanggal pengambilan dari daftar pilihan.
  • Summary: Tinjau pilihan, lalu kirim atau batalkan pesanan.

Tambahkan class enum untuk menentukan rute.

  1. Di CupcakeScreen.kt, di atas composable CupcakeAppBar, tambahkan class enum bernama CupcakeScreen.
enum class CupcakeScreen() {

}
  1. Tambahkan empat kasus ke class enum: Start, Flavor, Pickup, dan Summary.
enum class CupcakeScreen() {
    Start,
    Flavor,
    Pickup,
    Summary
}

Menambahkan NavHost ke aplikasi Anda

NavHost adalah Composable yang menampilkan tujuan composable lainnya, berdasarkan rute tertentu. Misalnya, jika rutenya adalah Flavor, NavHost akan menampilkan layar untuk memilih rasa cupcake. Jika rutenya adalah Summary, aplikasi akan menampilkan layar ringkasan.

Sintaksis untuk NavHost sama seperti Composable lainnya.

fae7688d6dd53de9.png

Ada dua parameter penting.

  • navController: Instance dari class NavHostController. Anda dapat menggunakan objek ini untuk berpindah antarlayar, misalnya, dengan memanggil metode navigate() untuk menuju ke tujuan lain. Anda dapat memperoleh NavHostController dengan memanggil rememberNavController() dari fungsi composable.
  • startDestination: Rute string yang menentukan tujuan yang ditampilkan secara default saat aplikasi pertama kali menampilkan NavHost. Untuk aplikasi Cupcake, seharusnya ini adalah rute Start.

Seperti composable lainnya, NavHost juga menggunakan parameter modifier.

Anda akan menambahkan NavHost ke composable CupcakeApp di CupcakeScreen.kt. Pertama, Anda memerlukan referensi untuk pengontrol navigasi. Anda dapat menggunakan pengontrol navigasi di NavHost yang Anda tambahkan sekarang dan AppBar yang akan Anda tambahkan pada langkah berikutnya. Oleh karena itu, Anda harus mendeklarasikan variabel dalam composable CupcakeApp().

  1. Buka CupcakeScreen.kt.
  2. Dalam Scaffold, di bawah variabel uiState, tambahkan composable NavHost.
import androidx.navigation.compose.NavHost

Scaffold(
    ...
) { innerPadding ->
    val uiState by viewModel.uiState.collectAsState()

    NavHost()
}
  1. Teruskan variabel navController untuk parameter navController dan CupcakeScreen.Start.name untuk parameter startDestination. Teruskan pengubah yang diteruskan ke CupcakeApp() untuk parameter pengubah. Teruskan lambda akhir kosong untuk parameter akhir.
import androidx.compose.foundation.layout.padding

NavHost(
    navController = navController,
    startDestination = CupcakeScreen.Start.name,
    modifier = Modifier.padding(innerPadding)
) {

}

Menangani rute di NavHost Anda

Seperti composable lainnya, NavHost menggunakan jenis fungsi untuk kontennya.

f67974b7fb3f0377.png

Dalam fungsi konten NavHost, Anda memanggil fungsi composable(). Fungsi composable() memerlukan dua parameter.

  • route: String yang sesuai dengan nama rute. Ini dapat berupa string unik apa pun. Anda akan menggunakan properti nama konstanta enum CupcakeScreen.
  • content: Di sini Anda dapat memanggil composable yang ingin ditampilkan untuk rute yang diberikan.

Anda akan memanggil fungsi composable() satu kali untuk masing-masing dari keempat rute.

  1. Panggil fungsi composable(), dengan meneruskan CupcakeScreen.Start.name untuk route.
import androidx.navigation.compose.composable

NavHost(
    navController = navController,
    startDestination = CupcakeScreen.Start.name,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = CupcakeScreen.Start.name) {

    }
}
  1. Dalam lambda terakhir, panggil composable StartOrderScreen, dengan meneruskan quantityOptions untuk properti quantityOptions. Untuk kartu modifier di Modifier.fillMaxSize().padding(dimensionResource(R.dimen.padding_medium))
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.res.dimensionResource
import com.example.cupcake.ui.StartOrderScreen
import com.example.cupcake.data.DataSource

NavHost(
    navController = navController,
    startDestination = CupcakeScreen.Start.name,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = CupcakeScreen.Start.name) {
        StartOrderScreen(
            quantityOptions = DataSource.quantityOptions,
            modifier = Modifier
                .fillMaxSize()
                .padding(dimensionResource(R.dimen.padding_medium))
        )
    }
}
  1. Di bawah panggilan pertama ke composable(), panggil composable() lagi, dengan meneruskan CupcakeScreen.Flavor.name untuk route.
composable(route = CupcakeScreen.Flavor.name) {

}
  1. Dalam lambda terakhir, dapatkan referensi ke LocalContext.current, lalu simpan dalam variabel bernama context. Context adalah class abstrak yang implementasinya disediakan oleh sistem Android. Class ini memungkinkan akses ke resource dan class khusus aplikasi, serta up-call untuk operasi tingkat aplikasi seperti aktivitas peluncuran, dll. Anda dapat menggunakan variabel ini untuk mendapatkan string dari daftar ID resource dalam model tampilan untuk menampilkan daftar rasa.
import androidx.compose.ui.platform.LocalContext

composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
}
  1. Panggil composable SelectOptionScreen.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(

    )
}
  1. Layar rasa perlu menampilkan dan mengupdate subtotal saat pengguna memilih rasa. Teruskan uiState.price untuk parameter subtotal.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price
    )
}
  1. Layar rasa mendapatkan daftar rasa dari resource string aplikasi. Ubah daftar ID resource menjadi daftar string menggunakan fungsi map() dan memanggil context.resources.getString(id) untuk setiap rasa.
import com.example.cupcake.ui.SelectOptionScreen

composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        options = DataSource.flavors.map { id -> context.resources.getString(id) }
    )
}
  1. Untuk parameter onSelectionChanged, teruskan ekspresi lambda yang memanggil setFlavor() pada model tampilan, dengan meneruskan it (argumen yang diteruskan ke onSelectionChanged()). Untuk parameter modifier, teruskan Modifier.fillMaxHeight().
import androidx.compose.foundation.layout.fillMaxHeight
import com.example.cupcake.data.DataSource.flavors

composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        options = DataSource.flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) },
        modifier = Modifier.fillMaxHeight()
    )
}

Layar tanggal pengambilan mirip dengan layar rasa. Satu-satunya perbedaan adalah data yang diteruskan ke composable SelectOptionScreen.

  1. Panggil lagi fungsi composable() dengan meneruskan CupcakeScreen.Pickup.name untuk parameter route.
composable(route = CupcakeScreen.Pickup.name) {

}
  1. Di lambda terakhir, panggil composable SelectOptionScreen dan teruskan uiState.price untuk subtotal, seperti sebelumnya. Teruskan uiState.pickupOptions untuk parameter options dan ekspresi lambda yang memanggil setDate() di viewModel untuk parameter onSelectionChanged. Untuk parameter modifier, teruskan Modifier.fillMaxHeight().
SelectOptionScreen(
    subtotal = uiState.price,
    options = uiState.pickupOptions,
    onSelectionChanged = { viewModel.setDate(it) },
    modifier = Modifier.fillMaxHeight()
)
  1. Panggil composable() sekali lagi, dengan meneruskan CupcakeScreen.Summary.name untuk route.
composable(route = CupcakeScreen.Summary.name) {

}
  1. Di lambda terakhir, panggil composable OrderSummaryScreen(), dengan meneruskan variabel uiState untuk parameter orderUiState. Untuk parameter modifier, teruskan Modifier.fillMaxHeight().
import com.example.cupcake.ui.OrderSummaryScreen

composable(route = CupcakeScreen.Summary.name) {
    OrderSummaryScreen(
        orderUiState = uiState,
        modifier = Modifier.fillMaxHeight()
    )
}

Seperti itulah cara menyiapkan NavHost. Di bagian berikutnya, Anda akan membuat aplikasi mengubah rute dan menavigasi antarlayar saat pengguna mengetuk setiap tombol.

5. Menavigasi antara rute

Setelah Anda menentukan rute dan memetakannya ke composable di NavHost, kini saatnya menavigasi antarlayar. NavHostController, yang merupakan properti navController dari panggilan rememberNavController(), bertanggung jawab untuk navigasi di antara rute. Namun, perhatikan bahwa properti ini ditentukan dalam composable CupcakeApp. Anda memerlukan cara untuk mengaksesnya dari berbagai layar di aplikasi Anda.

Mudah, kan? Cukup teruskan navController sebagai parameter ke setiap composable.

Meskipun pendekatan ini berhasil, ini bukan cara yang ideal untuk merancang aplikasi. Manfaat menggunakan NavHost untuk menangani navigasi aplikasi Anda adalah logika navigasi disimpan terpisah dari masing-masing UI. Opsi ini menghindari beberapa kelemahan utama dalam meneruskan navController sebagai parameter.

  • Logika navigasi disimpan di satu tempat, yang bisa membuat kode Anda lebih mudah dikelola dan mencegah bug dengan tidak sengaja memberikan kontrol navigasi bebas pada setiap layar di aplikasi.
  • Di aplikasi yang perlu berfungsi pada berbagai faktor bentuk (seperti ponsel mode potret, ponsel foldable, atau tablet layar besar), tombol mungkin atau mungkin tidak memicu navigasi, bergantung pada tata letak aplikasi. Masing-masing layar harus bersifat mandiri dan tidak perlu mengetahui layar lain di aplikasi.

Sebagai gantinya, pendekatan kita adalah meneruskan jenis fungsi ke setiap composable untuk apa yang akan terjadi saat pengguna mengklik tombol. Dengan demikian, composable dan setiap composable turunannya memutuskan kapan harus memanggil fungsi. Namun, logika navigasi tidak ditampilkan ke setiap layar di aplikasi Anda. Semua perilaku navigasi ditangani di NavHost.

Menambahkan pengendali tombol ke StartOrderScreen

Anda akan memulai dengan menambahkan parameter jenis fungsi yang dipanggil saat salah satu tombol kuantitas ditekan di layar pertama. Fungsi ini diteruskan ke dalam composable StartOrderScreen dan bertanggung jawab untuk memperbarui model tampilan dan membuka layar berikutnya.

  1. Buka StartOrderScreen.kt.
  2. Di bawah parameter quantityOptions, dan sebelum parameter pengubah, tambahkan parameter bernama onNextButtonClicked dari jenis () -> Unit.
@Composable
fun StartOrderScreen(
    quantityOptions: List<Pair<Int, Int>>,
    onNextButtonClicked: () -> Unit,
    modifier: Modifier = Modifier
){
    ...
}
  1. Setelah composable StartOrderScreen mendapatkan nilai untuk onNextButtonClicked, cari StartOrderPreview lalu teruskan isi lambda kosong ke parameter onNextButtonClicked.
@Preview
@Composable
fun StartOrderPreview() {
    CupcakeTheme {
        StartOrderScreen(
            quantityOptions = DataSource.quantityOptions,
            onNextButtonClicked = {},
            modifier = Modifier
                .fillMaxSize()
                .padding(dimensionResource(R.dimen.padding_medium))
        )
    }
}

Setiap tombol sesuai dengan jumlah cupcake yang berbeda. Anda akan memerlukan informasi ini agar fungsi yang diteruskan untuk onNextButtonClicked dapat mengupdate model tampilan.

  1. Ubah jenis parameter onNextButtonClicked untuk mengambil parameter Int.
onNextButtonClicked: (Int) -> Unit,

Agar Int diteruskan saat memanggil onNextButtonClicked(), lihat jenis parameter quantityOptions.

Jenisnya adalah List<Pair<Int, Int>> atau daftar Pair<Int, Int>. Anda mungkin tidak mengenal jenis Pair, tetapi sama seperti namanya, ini adalah sepasang nilai. Pair menggunakan dua parameter jenis generik. Dalam hal ini, keduanya adalah jenis Int.

8326701a77706258.png

Setiap item dalam pasangan diakses oleh properti pertama atau properti kedua. Untuk parameter quantityOptions composable StartOrderScreen, Int pertama adalah ID resource untuk string yang ditampilkan di setiap tombol. Int kedua adalah jumlah cupcake sebenarnya.

Kita akan meneruskan properti kedua dari pasangan yang dipilih saat memanggil fungsi onNextButtonClicked().

  1. Temukan ekspresi lambda kosong untuk parameter onClick dari SelectQuantityButton.
quantityOptions.forEach { item ->
    SelectQuantityButton(
        labelResourceId = item.first,
        onClick = {}
    )
}
  1. Dalam ekspresi lambda, panggil onNextButtonClicked dengan meneruskan item.second, yaitu jumlah cupcake.
quantityOptions.forEach { item ->
    SelectQuantityButton(
        labelResourceId = item.first,
        onClick = { onNextButtonClicked(item.second) }
    )
}

Menambahkan pengendali tombol ke SelectOptionScreen

  1. Di bawah parameter onSelectionChanged composable SelectOptionScreendi SelectOptionScreen.kt, tambahkan parameter bernama onCancelButtonClicked dari jenis () -> Unit dengan nilai default {}.
@Composable
fun SelectOptionScreen(
    subtotal: String,
    options: List<String>,
    onSelectionChanged: (String) -> Unit = {},
    onCancelButtonClicked: () -> Unit = {},
    modifier: Modifier = Modifier
)
  1. Di bawah parameter onCancelButtonClicked, tambahkan parameter lain dari jenis () -> Unit bernama onNextButtonClicked dengan nilai default {}.
@Composable
fun SelectOptionScreen(
    subtotal: String,
    options: List<String>,
    onSelectionChanged: (String) -> Unit = {},
    onCancelButtonClicked: () -> Unit = {},
    onNextButtonClicked: () -> Unit = {},
    modifier: Modifier = Modifier
)
  1. Teruskan onCancelButtonClicked untuk parameter onClick tombol batal.
OutlinedButton(
    modifier = Modifier.weight(1f),
    onClick = onCancelButtonClicked
) {
    Text(stringResource(R.string.cancel))
}
  1. Teruskan onNextButtonClicked untuk parameter onClick tombol berikutnya.
Button(
    modifier = Modifier.weight(1f),
    enabled = selectedValue.isNotEmpty(),
    onClick = onNextButtonClicked
) {
    Text(stringResource(R.string.next))
}

Menambahkan pengendali tombol ke SummaryScreen

Terakhir, tambahkan fungsi pengendali tombol untuk tombol Cancel dan Send pada layar ringkasan.

  1. Pada composable OrderSummaryScreen di SummaryScreen.kt, tambahkan parameter bernama onCancelButtonClicked dari jenis () -> Unit.
@Composable
fun OrderSummaryScreen(
    orderUiState: OrderUiState,
    onCancelButtonClicked: () -> Unit,
    modifier: Modifier = Modifier
){
    ...
}
  1. Tambahkan parameter lain dari jenis (String, String) -> Unit lalu beri nama onSendButtonClicked.
@Composable
fun OrderSummaryScreen(
    orderUiState: OrderUiState,
    onCancelButtonClicked: () -> Unit,
    onSendButtonClicked: (String, String) -> Unit,
    modifier: Modifier = Modifier
){
    ...
}
  1. Sekarang composable OrderSummaryScreen mendapatkan nilai untuk onSendButtonClicked dan onCancelButtonClicked. Cari OrderSummaryPreview, teruskan isi lambda kosong dengan dua parameter String ke onSendButtonClicked dan isi lambda kosong ke parameter onCancelButtonClicked.
@Preview
@Composable
fun OrderSummaryPreview() {
   CupcakeTheme {
       OrderSummaryScreen(
           orderUiState = OrderUiState(0, "Test", "Test", "$300.00"),
           onSendButtonClicked = { subject: String, summary: String -> },
           onCancelButtonClicked = {},
           modifier = Modifier.fillMaxHeight()
       )
   }
}
  1. Teruskan onSendButtonClicked untuk parameter onClick dari tombol Send. Teruskan newOrder dan orderSummary, dua variabel yang ditentukan sebelumnya di OrderSummaryScreen. String ini terdiri dari data aktual yang dapat dibagikan pengguna kepada aplikasi lain.
Button(
    modifier = Modifier.fillMaxWidth(),
    onClick = { onSendButtonClicked(newOrder, orderSummary) }
) {
    Text(stringResource(R.string.send))
}
  1. Teruskan onCancelButtonClicked untuk parameter onClick pada tombol Cancel.
OutlinedButton(
    modifier = Modifier.fillMaxWidth(),
    onClick = onCancelButtonClicked
) {
    Text(stringResource(R.string.cancel))
}

Untuk membuka rute lain, cukup panggil metode navigate() pada instance NavHostController Anda.

fc8aae3911a6a25d.png

Metode navigasi mengambil satu parameter: String yang sesuai dengan rute yang ditentukan di NavHost. Jika rute cocok dengan salah satu panggilan ke composable() di NavHost, aplikasi akan membuka layar tersebut.

Anda akan meneruskan fungsi yang memanggil navigate() saat pengguna menekan tombol pada layar Start, Flavor, dan Pickup.

  1. Di CupcakeScreen.kt, temukan panggilan ke composable() untuk layar mulai. Untuk parameter onNextButtonClicked, teruskan ekspresi lambda.
StartOrderScreen(
    quantityOptions = DataSource.quantityOptions,
    onNextButtonClicked = {
    }
)

Ingat properti Int yang diteruskan ke fungsi ini untuk jumlah cupcake? Sebelum membuka layar berikutnya, Anda harus mengupdate model tampilan agar aplikasi menampilkan subtotal yang benar.

  1. Panggil setQuantity pada viewModel, yang meneruskan it.
onNextButtonClicked = {
    viewModel.setQuantity(it)
}
  1. Panggil navigate() pada navController, yang meneruskan CupcakeScreen.Flavor.name untuk route.
onNextButtonClicked = {
    viewModel.setQuantity(it)
    navController.navigate(CupcakeScreen.Flavor.name)
}
  1. Untuk parameter onNextButtonClicked di layar rasa, cukup teruskan lambda yang memanggil navigate(), dengan meneruskan CupcakeScreen.Pickup.name untuk route.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
        options = DataSource.flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) },
        modifier = Modifier.fillMaxHeight()
    )
}
  1. Teruskan lambda kosong untuk onCancelButtonClicked yang akan Anda implementasikan berikutnya.
SelectOptionScreen(
    subtotal = uiState.price,
    onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
    onCancelButtonClicked = {},
    options = DataSource.flavors.map { id -> context.resources.getString(id) },
    onSelectionChanged = { viewModel.setFlavor(it) },
    modifier = Modifier.fillMaxHeight()
)
  1. Untuk parameter onNextButtonClicked di layar pengambilan, teruskan lambda yang memanggil navigate(), dengan meneruskan CupcakeScreen.Summary.name untuk route.
composable(route = CupcakeScreen.Pickup.name) {
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
        options = uiState.pickupOptions,
        onSelectionChanged = { viewModel.setDate(it) },
        modifier = Modifier.fillMaxHeight()
    )
}
  1. Sekali lagi, teruskan lambda kosong untuk onCancelButtonClicked().
SelectOptionScreen(
    subtotal = uiState.price,
    onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
    onCancelButtonClicked = {},
    options = uiState.pickupOptions,
    onSelectionChanged = { viewModel.setDate(it) },
    modifier = Modifier.fillMaxHeight()
)
  1. Untuk OrderSummaryScreen, teruskan lambda kosong untuk onCancelButtonClicked dan onSendButtonClicked. Tambahkan parameter untuk subject dan summary yang diteruskan ke onSendButtonClicked, yang akan segera Anda implementasikan.
composable(route = CupcakeScreen.Summary.name) {
    OrderSummaryScreen(
        orderUiState = uiState,
        onCancelButtonClicked = {},
        onSendButtonClicked = { subject: String, summary: String ->

        },
        modifier = Modifier.fillMaxHeight()
    )
}

Sekarang Anda dapat membuka setiap layar aplikasi. Perhatikan bahwa dengan memanggil navigate(), layar tidak hanya berubah, tetapi juga ditempatkan di atas data sebelumnya. Selain itu, saat menekan tombol kembali sistem, Anda dapat kembali ke layar sebelumnya.

Aplikasi menumpuk setiap layar di atas layar sebelumnya, dan tombol kembali (bade5f3ecb71e4a2.png) dapat menghapusnya. Histori layar dari startDestination di bagian bawah hingga layar teratas yang baru saja ditampilkan dikenal sebagai data sebelumnya.

Membuka layar awal

Tidak seperti tombol kembali sistem, tombol Cancel tidak kembali ke layar sebelumnya. Sebagai gantinya, tombol ini akan menghilangkan—menghapus—semua layar dari data sebelumnya dan kembali ke layar awal.

Anda dapat melakukannya dengan memanggil metode popBackStack().

2f382e5eb319b4b8.png

Metode popBackStack() memerlukan dua parameter.

  • route: String yang mewakili rute tujuan yang akan dinavigasikan kembali.
  • inclusive: Nilai Boolean yang, jika benar, juga akan menampilkan (menghapus) rute yang ditentukan. Jika salah, popBackStack() akan menghapus semua tujuan di atas—tetapi tidak termasuk—tujuan awal, sehingga membiarkannya sebagai layar teratas yang terlihat oleh pengguna.

Saat pengguna menekan tombol Cancel pada layar mana pun, aplikasi akan mereset status dalam model tampilan dan memanggil popBackStack(). Anda akan menerapkan metode untuk melakukan ini terlebih dahulu, lalu meneruskannya untuk parameter yang sesuai di ketiga layar dengan tombol Cancel.

  1. Setelah fungsi CupcakeApp(), tentukan fungsi pribadi yang disebut cancelOrderAndNavigateToStart().
private fun cancelOrderAndNavigateToStart() {
}
  1. Tambahkan dua parameter: viewModel dari jenis OrderViewModel, dan navController dari jenis NavHostController.
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
}
  1. Di isi fungsi, panggil resetOrder() pada viewModel.
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
    viewModel.resetOrder()
}
  1. Panggil popBackStack() pada navController, yang meneruskan CupcakeScreen.Start.name untuk route, dan false untuk inclusive.
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
    viewModel.resetOrder()
    navController.popBackStack(CupcakeScreen.Start.name, inclusive = false)
}
  1. Di composable CupcakeApp(), teruskan cancelOrderAndNavigateToStart untuk parameter onCancelButtonClicked dari dua composable SelectOptionScreen dan composable OrderSummaryScreen.
composable(route = CupcakeScreen.Start.name) {
    StartOrderScreen(
        quantityOptions = DataSource.quantityOptions,
        onNextButtonClicked = {
            viewModel.setQuantity(it)
            navController.navigate(CupcakeScreen.Flavor.name)
        },
        modifier = Modifier
            .fillMaxSize()
            .padding(dimensionResource(R.dimen.padding_medium))
    )
}
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
        onCancelButtonClicked = {
            cancelOrderAndNavigateToStart(viewModel, navController)
        },
        options = DataSource.flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) },
        modifier = Modifier.fillMaxHeight()
    )
}
composable(route = CupcakeScreen.Pickup.name) {
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
        onCancelButtonClicked = {
            cancelOrderAndNavigateToStart(viewModel, navController)
        },
        options = uiState.pickupOptions,
        onSelectionChanged = { viewModel.setDate(it) },
        modifier = Modifier.fillMaxHeight()
    )
}
composable(route = CupcakeScreen.Summary.name) {
    OrderSummaryScreen(
        orderUiState = uiState,
        onCancelButtonClicked = {
            cancelOrderAndNavigateToStart(viewModel, navController)
        },
        onSendButtonClicked = { subject: String, summary: String ->

        },
        modifier = Modifier.fillMaxHeight()
   )
}
  1. Jalankan aplikasi Anda dan uji apakah menekan tombol Cancel di layar mana pun akan mengarahkan pengguna kembali ke layar pertama.

6. Membuka aplikasi lain

Sejauh ini, Anda telah mempelajari cara membuka layar yang berbeda di aplikasi dan cara kembali ke layar utama. Tinggal satu langkah lagi untuk menerapkan navigasi di aplikasi Cupcake. Pada layar ringkasan pesanan, pengguna dapat mengirimkan pesanannya ke aplikasi lain. Pilihan ini menampilkan ShareSheet—komponen antarmuka pengguna yang menutupi bagian bawah layar—yang menampilkan opsi berbagi.

UI ini bukan bagian dari aplikasi Cupcake, melainkan disediakan oleh sistem operasi Android. UI sistem, seperti layar berbagi, tidak dipanggil oleh navController. Sebagai gantinya, Anda menggunakan Intent.

Intent adalah permintaan bagi sistem untuk melakukan beberapa tindakan, umumnya menghadirkan aktivitas baru. Ada banyak intent yang berbeda, dan sebaiknya Anda merujuk ke dokumentasi untuk mendapatkan daftar yang komprehensif. Namun, kita tertarik dengan yang disebut ACTION_SEND. Anda dapat memberi intent ini beberapa data, seperti string, dan menyajikan tindakan berbagi yang sesuai untuk data tersebut.

Proses dasar untuk menyiapkan intent adalah sebagai berikut:

  1. Buat objek intent dan tentukan intent tersebut, seperti ACTION_SEND.
  2. Tentukan jenis data tambahan yang dikirim dengan intent. Untuk teks sederhana, Anda dapat menggunakan "text/plain", meskipun jenis lain, seperti "image/*" atau "video/*", tersedia.
  3. Teruskan data tambahan ke intent, seperti teks atau gambar untuk dibagikan, dengan memanggil metode putExtra(). Intent ini akan memerlukan dua tambahan: EXTRA_SUBJECT dan EXTRA_TEXT.
  4. Panggil metode konteks startActivity(), dengan meneruskan aktivitas yang dibuat dari intent.

Kami akan memandu Anda dalam membuat intent tindakan berbagi, tetapi prosesnya sama untuk jenis intent lainnya. Untuk project selanjutnya, sebaiknya Anda membaca dokumentasi yang diperlukan untuk jenis data tertentu dan menambahkan yang diperlukan.

Selesaikan langkah-langkah berikut untuk membuat intent guna mengirim pesanan cupcake ke aplikasi lain:

  1. Di CupcakeScreen.kt, di bawah composable CupcakeApp, buat fungsi pribadi bernama shareOrder().
private fun shareOrder()
  1. Tambahkan parameter bernama context dari jenis Context.
import android.content.Context

private fun shareOrder(context: Context) {
}
  1. Tambahkan dua parameter String: subject dan summary. String ini akan ditampilkan di lembar tindakan berbagi.
private fun shareOrder(context: Context, subject: String, summary: String) {
}
  1. Dalam isi fungsi, buat Intent bernama intent, dan teruskan Intent.ACTION_SEND sebagai argumen.
import android.content.Intent

val intent = Intent(Intent.ACTION_SEND)

Karena Anda hanya perlu mengonfigurasi objek Intent ini sekali, Anda dapat membuat beberapa baris kode berikutnya lebih ringkas menggunakan fungsi apply(), yang telah Anda pelajari di codelab sebelumnya.

  1. Panggil apply() pada Intent yang baru dibuat dan teruskan ekspresi lambda.
val intent = Intent(Intent.ACTION_SEND).apply {

}
  1. Di isi lambda, tetapkan jenis ke "text/plain". Karena Anda melakukan ini dalam fungsi yang diteruskan ke apply(), Anda tidak perlu merujuk ke ID objek, intent.
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
}
  1. Panggil putExtra(), dengan meneruskan subjek untuk EXTRA_SUBJECT.
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_SUBJECT, subject)
}
  1. Panggil putExtra(), dengan meneruskan ringkasan untuk EXTRA_TEXT.
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_SUBJECT, subject)
    putExtra(Intent.EXTRA_TEXT, summary)
}
  1. Panggil metode startActivity() konteks.
context.startActivity(

)
  1. Dalam lambda yang diteruskan ke startActivity(), buat aktivitas dari Intent dengan memanggil metode class createChooser(). Teruskan intent untuk argumen pertama dan resource string new_cupcake_order.
context.startActivity(
    Intent.createChooser(
        intent,
        context.getString(R.string.new_cupcake_order)
    )
)
  1. Pada composable CupcakeApp, dalam panggilan ke composable() untuk CucpakeScreen.Summary.name, dapatkan referensi ke objek konteks sehingga Anda dapat meneruskannya ke fungsi shareOrder().
composable(route = CupcakeScreen.Summary.name) {
    val context = LocalContext.current

    ...
}
  1. Dalam isi lambda onSendButtonClicked(), panggil shareOrder() dengan meneruskan context, subject, dan summary sebagai argumen.
onSendButtonClicked = { subject: String, summary: String ->
    shareOrder(context, subject = subject, summary = summary)
}
  1. Jalankan aplikasi Anda dan jelajahi layar.

Saat mengklik Send Order to Another App, Anda akan melihat tindakan berbagi seperti Messaging dan Bluetooth di sheet bawah, beserta subjek dan ringkasan yang Anda berikan sebagai tambahan.

a32e016a6ccbf427.png

7. Membuat panel aplikasi merespons navigasi

Meskipun aplikasi Anda berfungsi dan dapat menavigasi ke dan dari setiap layar, masih ada sesuatu yang hilang dari screenshot di awal codelab ini. Panel aplikasi tidak merespons navigasi secara otomatis. Judul tidak diupdate saat aplikasi menuju ke rute baru atau menampilkan tombol Atas sebelum judul jika sesuai.

Kode awal menyertakan composable untuk mengelola AppBar yang bernama CupcakeAppBar. Setelah mengimplementasikan navigasi di aplikasi, Anda dapat menggunakan informasi dari data sebelumnya untuk menampilkan judul yang benar dan menampilkan tombol Atas jika sesuai. Composable CupcakeAppBar harus memperhatikan layar saat ini sehingga judul diupdate dengan tepat.

  1. Di enum CupcakeScreen di CupcakeScreen.kt, tambahkan parameter jenis Int bernama title menggunakan anotasi @StringRes.
import androidx.annotation.StringRes

enum class CupcakeScreen(@StringRes val title: Int) {
    Start,
    Flavor,
    Pickup,
    Summary
}
  1. Tambahkan nilai resource untuk setiap kasus enum, yang sesuai dengan teks judul untuk setiap layar. Gunakan app_name untuk layar Start, choose_flavor untuk layar Flavor, choose_pickup_date untuk layar Pickup, dan order_summary untuk layar Summary.
enum class CupcakeScreen(@StringRes val title: Int) {
    Start(title = R.string.app_name),
    Flavor(title = R.string.choose_flavor),
    Pickup(title = R.string.choose_pickup_date),
    Summary(title = R.string.order_summary)
}
  1. Tambahkan parameter bernama currentScreen dari jenis CupcakeScreen ke composable CupcakeAppBar.
fun CupcakeAppBar(
    currentScreen: CupcakeScreen,
    canNavigateBack: Boolean,
    navigateUp: () -> Unit = {},
    modifier: Modifier = Modifier
)
  1. Di dalam CupcakeAppBar, ganti nama aplikasi hard code dengan judul layar saat ini dengan meneruskan currentScreen.title ke panggilan ke stringResource() untuk parameter judul TopAppBar.
TopAppBar(
    title = { Text(stringResource(currentScreen.title)) },
    modifier = modifier,
    navigationIcon = {
        if (canNavigateBack) {
            IconButton(onClick = navigateUp) {
                Icon(
                    imageVector = Icons.Filled.ArrowBack,
                    contentDescription = stringResource(R.string.back_button)
                )
            }
        }
    }
)

Tombol Atas hanya muncul jika ada composable di data sebelumnya. Jika aplikasi tidak memiliki layar di data sebelumnya—StartOrderScreen ditampilkan—tombol Atas tidak akan ditampilkan. Untuk memeriksanya, Anda memerlukan referensi ke data sebelumnya.

  1. Pada composable CupcakeApp, di bawah variabel navController, buat variabel bernama backStackEntry dan panggil metode currentBackStackEntryAsState() dari navController menggunakan delegasi by.
import androidx.navigation.compose.currentBackStackEntryAsState

@Composable
fun CupcakeApp(
    viewModel: OrderViewModel = viewModel(),
    navController: NavHostController = rememberNavController()
){

    val backStackEntry by navController.currentBackStackEntryAsState()

    ...
}
  1. Konversikan judul layar saat ini menjadi nilai CupcakeScreen. Di bawah variabel backStackEntry, buat variabel menggunakan val bernama currentScreen yang sama dengan hasil pemanggilan fungsi class valueOf() dari CupcakeScreen, lalu teruskan rute tujuan backStackEntry. Gunakan operator elvis untuk memberikan nilai default CupcakeScreen.Start.name.
val currentScreen = CupcakeScreen.valueOf(
    backStackEntry?.destination?.route ?: CupcakeScreen.Start.name
)
  1. Teruskan nilai variabel currentScreen ke dalam parameter bernama sama dari composable CupcakeAppBar.
CupcakeAppBar(
    currentScreen = currentScreen,
    canNavigateBack = false,
    navigateUp = {}
)

Selama ada layar di belakang layar saat ini pada data sebelumnya, tombol Atas akan muncul. Anda dapat menggunakan ekspresi boolean untuk mengidentifikasi apakah tombol Atas akan muncul:

  1. Untuk parameter canNavigateBack, teruskan ekspresi boolean yang memeriksa apakah properti previousBackStackEntry dari navController tidak sama dengan null.
canNavigateBack = navController.previousBackStackEntry != null,
  1. Untuk benar-benar kembali ke layar sebelumnya, panggil metode navigateUp() navController.
navigateUp = { navController.navigateUp() }
  1. Jalankan aplikasi Anda.

Perhatikan bahwa judul AppBar sekarang diupdate untuk mencerminkan layar saat ini. Saat membuka layar selain StartOrderScreen, tombol Atas akan muncul dan membawa Anda kembali ke layar sebelumnya.

3fd023516061f522.gif

8. Mendapatkan kode solusi

Untuk mendownload kode codelab yang sudah selesai, Anda dapat menggunakan perintah git berikut:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-cupcake.git
$ cd basic-android-kotlin-compose-training-cupcake
$ git checkout navigation

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

Jika Anda ingin melihat kode solusi untuk codelab ini, lihat kode tersebut di GitHub.

9. Ringkasan

Selamat! Anda baru saja melakukan peningkatan dari aplikasi satu layar sederhana ke aplikasi multilayar yang kompleks menggunakan komponen Jetpack Navigation untuk berpindah di antara beberapa layar. Anda menentukan rute, menanganinya di NavHost, dan menggunakan parameter jenis fungsi untuk memisahkan logika navigasi dari setiap layar. Anda juga telah mempelajari cara mengirim data ke aplikasi lain menggunakan intent serta menyesuaikan panel aplikasi sebagai respons terhadap navigasi. Dalam unit mendatang, Anda akan terus menggunakan keterampilan ini saat mengerjakan beberapa aplikasi multilayar lainnya dengan kompleksitas yang semakin meningkat.

Mempelajari Lebih Lanjut