Latihan: Membangun aplikasi Superheroes

1. Sebelum memulai

Selamat! Di pembelajaran ini, Anda telah mempelajari dasar-dasar Desain Material dan cara menambahkan animasi sederhana ke aplikasi Anda. Sekarang saatnya menerapkan apa yang telah Anda pelajari.

Dalam kumpulan latihan ini, Anda akan menerapkan konsep yang telah dipelajari di pembelajaran ini dengan membuat aplikasi Superheroes. Aplikasi ini berfokus pada pembuatan komponen yang diperlukan untuk membangun daftar yang dapat di-scroll dan UI yang dipoles menggunakan prinsip Desain Material yang telah Anda pelajari di codelab Penerapan Tema Material dengan Jetpack Compose.

Kode solusi tersedia di bagian akhir, tetapi coba selesaikan latihan ini sebelum Anda memeriksanya. Pertimbangkan solusinya sebagai salah satu cara untuk menerapkan aplikasi. Ada banyak peningkatan yang dapat dilakukan, jadi jangan ragu untuk bereksperimen dan mencoba berbagai hal.

Selesaikan semua soal latihan tanpa terburu-buru. Sebaiknya manfaatkan waktu semaksimal mungkin untuk menyelesaikan setiap soal dengan cermat.

Prasyarat

Yang Anda butuhkan

  • Komputer yang dilengkapi akses internet dan Android Studio.

Yang akan Anda bangun

Aplikasi Superheroes yang menampilkan daftar pahlawan super.

Aplikasi final akan terlihat seperti berikut dalam tema terang dan gelap:

2. Memulai

Dalam tugas ini, Anda akan menyiapkan project dan membuat data model untuk pahlawan super.

  1. Buat project baru dengan template Empty Activity dan SDK minimum 24.
  2. Download aset untuk aplikasi: gambar pahlawan super dan logo aplikasi dari sini. Baca codelab Mengubah ikon aplikasi untuk mengingat kembali cara menambahkan ikon aplikasi. Baca codelab Membuat aplikasi Dice Roller interaktif untuk mengingat kembali cara menambahkan gambar ke aplikasi Anda.
  3. Download file font Cabin biasa dan Cabin tebal dari https://fonts.google.com. Jelajahi berbagai file font yang tersedia. Baca codelab Penerapan Tema Material dengan Jetpack Compose untuk menyesuaikan tipografi di aplikasi Anda.
  4. Buat class data untuk menyimpan data setiap pahlawan super. Buat paket baru bernama model untuk class data Hero guna mengatur kode Anda. Item daftar Anda mungkin terlihat seperti berikut:

268233a1e2b3b407.png

Setiap item daftar pahlawan super menampilkan tiga informasi unik: nama, deskripsi, dan gambar.

  1. Dalam paket model yang sama, buat file lain untuk semua informasi pahlawan yang ingin Anda tampilkan. Misalnya, nama, deskripsi, dan resource gambar. Berikut ini contoh set data yang akan menginspirasi Anda.
object HeroesRepository {
    val heroes = listOf(
        Hero(
            nameRes = R.string.hero1,
            descriptionRes = R.string.description1,
            imageRes = R.drawable.android_superhero1
        ),
        Hero(
            nameRes = R.string.hero2,
            descriptionRes = R.string.description2,
            imageRes = R.drawable.android_superhero2
        ),
        Hero(
            nameRes = R.string.hero3,
            descriptionRes = R.string.description3,
            imageRes = R.drawable.android_superhero3
        ),
        Hero(
            nameRes = R.string.hero4,
            descriptionRes = R.string.description4,
            imageRes = R.drawable.android_superhero4
        ),
        Hero(
            nameRes = R.string.hero5,
            descriptionRes = R.string.description5,
            imageRes = R.drawable.android_superhero5
        ),
        Hero(
            nameRes = R.string.hero6,
            descriptionRes = R.string.description6,
            imageRes = R.drawable.android_superhero6
        )
    )
}
  1. Tambahkan nama pahlawan serta string deskripsi dalam file strings.xml.
<resources>
    <string name="app_name">Superheroes</string>
    <string name="hero1">Nick the Night and Day</string>
    <string name="description1">The Jetpack Hero</string>
    <string name="hero2">Reality Protector</string>
    <string name="description2">Understands the absolute truth</string>
    <string name="hero3">Andre the Giant</string>
    <string name="description3">Mimics the light and night to blend in</string>
    <string name="hero4">Benjamin the Brave</string>
    <string name="description4">Harnesses the power of canary to develop bravely</string>
    <string name="hero5">Magnificent Maru</string>
    <string name="description5">Effortlessly glides in to save the day</string>
    <string name="hero6">Dynamic Yasmine</string>
    <string name="description6">Ability to shift to any form and energize</string>
</resources>

3. Penerapan Tema Material

Di bagian ini, Anda akan menambahkan palet warna, tipografi, dan bentuk aplikasi untuk meningkatkan tampilan dan nuansa aplikasi.

Warna, Jenis, dan Bentuk berikut hanyalah rekomendasi untuk tema tersebut. Jelajahi dan ubah berbagai skema warna.

Gunakan Material Theme Builder untuk membuat tema baru aplikasi.

Warna

ui.theme/Color.kt

import androidx.compose.ui.graphics.Color

val md_theme_light_primary = Color(0xFF466800)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFC6F181)
val md_theme_light_onPrimaryContainer = Color(0xFF121F00)
val md_theme_light_secondary = Color(0xFF596248)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFDDE6C6)
val md_theme_light_onSecondaryContainer = Color(0xFF161E0A)
val md_theme_light_tertiary = Color(0xFF396661)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFBCECE6)
val md_theme_light_onTertiaryContainer = Color(0xFF00201D)
val md_theme_light_error = Color(0xFFBA1A1A)
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_onErrorContainer = Color(0xFF410002)
val md_theme_light_background = Color(0xFFFEFCF5)
val md_theme_light_onBackground = Color(0xFF1B1C18)
val md_theme_light_surface = Color(0xFFFEFCF5)
val md_theme_light_onSurface = Color(0xFF1B1C18)
val md_theme_light_surfaceVariant = Color(0xFFE1E4D4)
val md_theme_light_onSurfaceVariant = Color(0xFF45483D)
val md_theme_light_outline = Color(0xFF75786C)
val md_theme_light_inverseOnSurface = Color(0xFFF2F1E9)
val md_theme_light_inverseSurface = Color(0xFF30312C)
val md_theme_light_inversePrimary = Color(0xFFABD468)
val md_theme_light_surfaceTint = Color(0xFF466800)
val md_theme_light_outlineVariant = Color(0xFFC5C8B9)
val md_theme_light_scrim = Color(0xFF000000)

val md_theme_dark_primary = Color(0xFFABD468)
val md_theme_dark_onPrimary = Color(0xFF223600)
val md_theme_dark_primaryContainer = Color(0xFF344E00)
val md_theme_dark_onPrimaryContainer = Color(0xFFC6F181)
val md_theme_dark_secondary = Color(0xFFC1CAAB)
val md_theme_dark_onSecondary = Color(0xFF2B331D)
val md_theme_dark_secondaryContainer = Color(0xFF414A32)
val md_theme_dark_onSecondaryContainer = Color(0xFFDDE6C6)
val md_theme_dark_tertiary = Color(0xFFA0D0CA)
val md_theme_dark_onTertiary = Color(0xFF013733)
val md_theme_dark_tertiaryContainer = Color(0xFF1F4E4A)
val md_theme_dark_onTertiaryContainer = Color(0xFFBCECE6)
val md_theme_dark_error = Color(0xFFFFB4AB)
val md_theme_dark_errorContainer = Color(0xFF93000A)
val md_theme_dark_onError = Color(0xFF690005)
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
val md_theme_dark_background = Color(0xFF1B1C18)
val md_theme_dark_onBackground = Color(0xFFE4E3DB)
val md_theme_dark_surface = Color(0xFF1B1C18)
val md_theme_dark_onSurface = Color(0xFFE4E3DB)
val md_theme_dark_surfaceVariant = Color(0xFF45483D)
val md_theme_dark_onSurfaceVariant = Color(0xFFC5C8B9)
val md_theme_dark_outline = Color(0xFF8F9285)
val md_theme_dark_inverseOnSurface = Color(0xFF1B1C18)
val md_theme_dark_inverseSurface = Color(0xFFE4E3DB)
val md_theme_dark_inversePrimary = Color(0xFF466800)
val md_theme_dark_surfaceTint = Color(0xFFABD468)
val md_theme_dark_outlineVariant = Color(0xFF45483D)
val md_theme_dark_scrim = Color(0xFF000000)

Bentuk

ui.theme/Shape.kt

import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp

val Shapes = Shapes(
    small = RoundedCornerShape(8.dp),
    medium = RoundedCornerShape(16.dp),
    large = RoundedCornerShape(16.dp)
)

Tipografi

ui.theme/Type.kt

import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.example.superheroes.R

val Cabin = FontFamily(
    Font(R.font.cabin_regular, FontWeight.Normal),
    Font(R.font.cabin_bold, FontWeight.Bold)
)
// Set of Material typography styles to start with
val Typography = Typography(
    bodyLarge = TextStyle(
        fontFamily = Cabin,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.5.sp
    ),
    displayLarge = TextStyle(
        fontFamily = Cabin,
        fontWeight = FontWeight.Normal,
        fontSize = 30.sp
    ),
    displayMedium = TextStyle(
        fontFamily = Cabin,
        fontWeight = FontWeight.Bold,
        fontSize = 20.sp
    ),
    displaySmall = TextStyle(
        fontFamily = Cabin,
        fontWeight = FontWeight.Bold,
        fontSize = 20.sp
    )
)

Tema

ui.theme/Theme.kt

import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat

private val LightColors = lightColorScheme(
   primary = md_theme_light_primary,
   onPrimary = md_theme_light_onPrimary,
   primaryContainer = md_theme_light_primaryContainer,
   onPrimaryContainer = md_theme_light_onPrimaryContainer,
   secondary = md_theme_light_secondary,
   onSecondary = md_theme_light_onSecondary,
   secondaryContainer = md_theme_light_secondaryContainer,
   onSecondaryContainer = md_theme_light_onSecondaryContainer,
   tertiary = md_theme_light_tertiary,
   onTertiary = md_theme_light_onTertiary,
   tertiaryContainer = md_theme_light_tertiaryContainer,
   onTertiaryContainer = md_theme_light_onTertiaryContainer,
   error = md_theme_light_error,
   errorContainer = md_theme_light_errorContainer,
   onError = md_theme_light_onError,
   onErrorContainer = md_theme_light_onErrorContainer,
   background = md_theme_light_background,
   onBackground = md_theme_light_onBackground,
   surface = md_theme_light_surface,
   onSurface = md_theme_light_onSurface,
   surfaceVariant = md_theme_light_surfaceVariant,
   onSurfaceVariant = md_theme_light_onSurfaceVariant,
   outline = md_theme_light_outline,
   inverseOnSurface = md_theme_light_inverseOnSurface,
   inverseSurface = md_theme_light_inverseSurface,
   inversePrimary = md_theme_light_inversePrimary,
   surfaceTint = md_theme_light_surfaceTint,
   outlineVariant = md_theme_light_outlineVariant,
   scrim = md_theme_light_scrim,
)

private val DarkColors = darkColorScheme(
   primary = md_theme_dark_primary,
   onPrimary = md_theme_dark_onPrimary,
   primaryContainer = md_theme_dark_primaryContainer,
   onPrimaryContainer = md_theme_dark_onPrimaryContainer,
   secondary = md_theme_dark_secondary,
   onSecondary = md_theme_dark_onSecondary,
   secondaryContainer = md_theme_dark_secondaryContainer,
   onSecondaryContainer = md_theme_dark_onSecondaryContainer,
   tertiary = md_theme_dark_tertiary,
   onTertiary = md_theme_dark_onTertiary,
   tertiaryContainer = md_theme_dark_tertiaryContainer,
   onTertiaryContainer = md_theme_dark_onTertiaryContainer,
   error = md_theme_dark_error,
   errorContainer = md_theme_dark_errorContainer,
   onError = md_theme_dark_onError,
   onErrorContainer = md_theme_dark_onErrorContainer,
   background = md_theme_dark_background,
   onBackground = md_theme_dark_onBackground,
   surface = md_theme_dark_surface,
   onSurface = md_theme_dark_onSurface,
   surfaceVariant = md_theme_dark_surfaceVariant,
   onSurfaceVariant = md_theme_dark_onSurfaceVariant,
   outline = md_theme_dark_outline,
   inverseOnSurface = md_theme_dark_inverseOnSurface,
   inverseSurface = md_theme_dark_inverseSurface,
   inversePrimary = md_theme_dark_inversePrimary,
   surfaceTint = md_theme_dark_surfaceTint,
   outlineVariant = md_theme_dark_outlineVariant,
   scrim = md_theme_dark_scrim,
)

@Composable
fun SuperheroesTheme(
   darkTheme: Boolean = isSystemInDarkTheme(),
   // Dynamic color is available on Android 12+
   // Dynamic color in this app is turned off for learning purposes
   dynamicColor: Boolean = false,
   content: @Composable () -> Unit
) {
   val colorScheme = when {
       dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
           val context = LocalContext.current
           if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
       }

       darkTheme -> DarkColors
       else -> LightColors
   }
   val view = LocalView.current
   if (!view.isInEditMode) {
       SideEffect {
           val window = (view.context as Activity).window
           window.statusBarColor = colorScheme.background.toArgb()
           WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
       }
   }

   MaterialTheme(
       colorScheme = colorScheme,
       typography = Typography,
       shapes = Shapes,
       content = content
   )
}

4. Menampilkan daftar

Langkah pertama dalam membuat daftar adalah membuat item daftar.

  1. Buat file dengan nama HeroesScreen.kt, pada paket com.example.superheroes. Anda akan membuat item daftar dan composable daftar dalam file ini.
  2. Buat composable untuk menampilkan item daftar pahlawan super, yang terlihat seperti screenshot dan spesifikasi UI berikut. 268233a1e2b3b407.png

Ikuti spesifikasi UI ini atau berkreasilah dan desain item daftar Anda sendiri:

  • Elevasi kartu adalah 2dp
  • Tinggi item daftar adalah 72dp dengan padding 16dp
  • Radius klip item daftar adalah 16dp
  • Tata letak Box dengan gambar dengan ukuran 72dp
  • Radius klip gambar adalah 8dp
  • Ruang antara gambar dan teks adalah 16dp
  • Gaya untuk nama pahlawan super adalah DisplaySmall
  • Gaya untuk deskripsi pahlawan super adalah BodyLarge

Pelajari berbagai opsi padding dan ukuran, sesuai dengan panduan Material 3, padding harus berupa kelipatan 4dp.

3b073896adfdcd7a.png

6affe74f9559dc90.png

Membuat kolom lambat

  1. Buat composable lain yang mengambil daftar pahlawan dan menampilkan daftar. Di sinilah Anda menggunakan LazyColumn.
  2. Gunakan spesifikasi UI berikut untuk padding.

af5116f770dd0ad.png

Setelah Anda menyelesaikan implementasi, aplikasi Anda akan terlihat seperti screenshot berikut:

Layar ponsel menampilkan daftar tanpa panel aplikasi atas

5. Menambahkan panel aplikasi Atas

Tambahkan panel aplikasi atas untuk aplikasi Anda.

  1. Di MainActivity.kt, tambahkan composable untuk menampilkan panel aplikasi atas. Tambahkan teks ke panel aplikasi atas; dapat berupa nama aplikasi. Sejajarkan di tengah secara horizontal dan vertikal.
  2. Anda dapat menetapkan panel aplikasi atas dengan gaya sebagai DisplayLarge.

2e8eeb35ac3e631b.png

  1. Gunakan scaffold untuk menampilkan panel aplikasi atas. Lihat dokumentasi Panel aplikasi atas – Desain Material 3 jika diperlukan.

Menyesuaikan warna status bar

Untuk membuat aplikasi ditampilkan dalam layar penuh, Anda dapat menyesuaikan warna status bar agar cocok dengan warna latar belakang.

  1. Di Theme.kt, tambahkan metode baru ini untuk mengubah warna status bar dan menu navigasi layar penuh.
/**
 * Sets up edge-to-edge for the window of this [view]. The system icon colors are set to either
 * light or dark depending on whether the [darkTheme] is enabled or not.
 */
private fun setUpEdgeToEdge(view: View, darkTheme: Boolean) {
    val window = (view.context as Activity).window
    WindowCompat.setDecorFitsSystemWindows(window, false)
    window.statusBarColor = Color.Transparent.toArgb()
    val navigationBarColor = when {
        Build.VERSION.SDK_INT >= 29 -> Color.Transparent.toArgb()
        Build.VERSION.SDK_INT >= 26 -> Color(0xFF, 0xFF, 0xFF, 0x63).toArgb()
        // Min sdk version for this app is 24, this block is for SDK versions 24 and 25
        else -> Color(0x00, 0x00, 0x00, 0x50).toArgb()
    }
    window.navigationBarColor = navigationBarColor
    val controller = WindowCompat.getInsetsController(window, view)
    controller.isAppearanceLightStatusBars = !darkTheme
    controller.isAppearanceLightNavigationBars = !darkTheme
}
  1. Di fungsi SuperheroesTheme(), panggil fungsi setUpEdgeToEdge() dari dalam blok SideEffect.
fun SuperheroesTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    // Dynamic color in this app is turned off for learning purposes
    dynamicColor: Boolean = false,
    content: @Composable () -> Unit
) {
    //...
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            setUpEdgeToEdge(view, darkTheme)
        }
    }

    //...
}

6. Mendapatkan kode solusi

Guna mendownload kode untuk codelab yang sudah selesai, Anda dapat menggunakan perintah git ini:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-superheroes.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.