Thực hành: Tạo ứng dụng Superheroes.

1. Trước khi bắt đầu

Xin chúc mừng! Trong lộ trình này, bạn đã tìm hiểu những thông tin cơ bản về Material Design và cách thêm ảnh động đơn giản vào ứng dụng. Giờ là lúc thực hành các kiến thức đã học.

Trong bộ bài tập thực hành này, bạn sẽ tạo ứng dụng Superheroes (Siêu anh hùng) dựa trên các kiến thức đã học được ở lộ trình này. Đây là ứng dụng tập trung vào việc tạo các thành phần cần thiết để thiết kế một danh sách có thể cuộn cũng như một giao diện người dùng bắt mắt bằng cách sử dụng các nguyên tắc của Material Design mà bạn đã học ở lớp học lập trình Tuỳ chỉnh giao diện Material bằng Jetpack Compose.

Chúng tôi sẽ cung cấp mã giải pháp ở phần cuối. Tuy nhiên, bạn nên giải bài tập trước khi xem đáp án. Hãy xem các giải pháp này như một cách để triển khai ứng dụng. Có rất nhiều điểm cần cải thiện, vì thế, hãy tự do khám phá và thử nhiều thứ.

Hãy giải quyết các vấn đề theo tốc độ mà bạn cảm thấy thoải mái. Bạn nên dành đủ thời gian cần thiết để giải quyết từng vấn đề một cách thấu đáo.

Điều kiện tiên quyết

Bạn cần có

  • Máy tính có kết nối Internet và đã cài đặt Android Studio.

Sản phẩm bạn sẽ tạo ra

Ứng dụng Superheroes hiển thị danh sách các siêu anh hùng.

Ứng dụng hoàn thiện sẽ có hình thức như sau trong cả giao diện sáng lẫn tối:

2. Bắt đầu

Trong bài này, bạn sẽ thiết lập dự án và tạo dữ liệu giả cho các siêu anh hùng.

  1. Tạo một dự án mới có mẫu Empty Activity (Hoạt động trống) và SDK tối thiểu là 24.
  2. Tải thành phần của ứng dụng xuống: hình ảnh siêu anh hùng và biểu trưng của ứng dụng tại đây. Tham khảo lớp học lập trình Thay đổi biểu tượng ứng dụng để tìm hiểu lại cách thêm biểu tượng ứng dụng. Tham khảo lớp học lập trình Tạo ứng dụng Dice Roller (Đổ xúc xắc) có thể tương tác để tìm hiểu lại cách thêm hình ảnh vào ứng dụng.
  3. Tải các tệp phông chữ Cabin in thường và in đậm tại https://fonts.google.com. Khám phá các tệp phông chữ có sẵn. Tham khảo lớp học lập trình Tuỳ chỉnh giao diện Material bằng Jetpack Compose để tuỳ chỉnh kiểu chữ trong ứng dụng.
  4. Hãy tạo một lớp dữ liệu để lưu trữ dữ liệu của từng siêu anh hùng. Tạo một gói mới tên là model cho lớp dữ liệu Hero để sắp xếp mã của bạn. Mục danh sách có thể có dạng như sau:

268233a1e2b3b407.png

Mỗi mục trong danh sách siêu anh hùng cho biết 3 thông tin riêng biệt: tên, nội dung mô tả và hình ảnh.

  1. Trong cùng một gói model, hãy tạo một tệp khác cho mọi thông tin mà bạn muốn hiển thị về các anh hùng. Ví dụ: tên, mô tả và tài nguyên hình ảnh. Dưới đây là một tập dữ liệu mẫu để bạn tham khảo cảm hứng.
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. Thêm các chuỗi mô tả và tên anh hùng trong tệp 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. Tuỳ chỉnh giao diện Material

Trong phần này, bạn sẽ thêm bảng màu, kiểu chữ và hình dạng của ứng dụng để cải thiện giao diện của ứng dụng.

Màu, Loại và Hình dạng dưới đây chỉ là các đề xuất dành cho giao diện. Hãy khám phá và sửa đổi các bảng phối màu khác nhau.

Sử dụng Trình tạo giao diện Material để tạo giao diện mới cho ứng dụng.

Màu

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)

Hình dạng

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)
)

Kiểu chữ

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
    )
)

Giao diện

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. Hiển thị danh sách

Bước đầu tiên để tạo danh sách là tạo một mục danh sách.

  1. Tạo một tệp có tên là HeroesScreen.kt trong gói com.example.superheroes. Bạn sẽ tạo một mục danh sách và các thành phần kết hợp của danh sách trong tệp này.
  2. Tạo thành phần kết hợp để đại diện cho một mục danh sách siêu anh hùng, giống như ảnh chụp màn hình và thông số giao diện người dùng sau đây. 268233a1e2b3b407.png

Dùng thông số kỹ thuật này cho giao diện người dùng hoặc sáng tạo và thiết kế mục danh sách của riêng bạn:

  • Độ cao của thẻ là 2dp
  • Chiều cao của mục danh sách là 72dp với khoảng đệm là 16dp
  • Bán kính clip của mục danh sách là 16dp
  • Bố cục Box với hình ảnh có kích thước là 72dp
  • Bán kính clip của hình ảnh là 8dp
  • Khoảng cách giữa hình ảnh và văn bản là 16dp
  • Kiểu tên của siêu anh hùng là DisplaySmall
  • Kiểu phần mô tả của siêu anh hùng là BodyLarge

Khám phá các lựa chọn về khoảng đệm và kích thước. Theo nguyên tắc của Material 3, khoảng đệm phải là số gia của 4dp.

3b073896adfdcd7a.png

6affe74f9559dc90.png

Tạo cột lazy

  1. Tạo một thành phần kết hợp khác có danh sách các anh hùng và hiển thị danh sách này. Đây là nơi bạn sử dụng LazyColumn.
  2. Sử dụng các thông số kỹ thuật của giao diện người dùng sau cho khoảng đệm.

af5116f770dd0ad.png

Sau khi hoàn thành việc triển khai, ứng dụng của bạn phải giống với ảnh chụp màn hình sau:

Màn hình điện thoại hiện danh sách mà không có thanh ứng dụng trên cùng

5. Thêm Thanh ứng dụng trên cùng

Thêm thanh ứng dụng trên cùng cho ứng dụng của bạn.

  1. Trong MainActivity.kt, hãy thêm một thành phần kết hợp để hiện thanh ứng dụng trên cùng. Thêm văn bản vào thanh ứng dụng trên cùng; nó có thể là tên ứng dụng. Căn giữa theo chiều ngang và chiều dọc.
  2. Bạn có thể đặt thanh ứng dụng trên cùng có kiểu DisplayLarge.

2e8eeb35ac3e631b.png

  1. Sử dụng scaffold để hiện thanh ứng dụng trên cùng. Tham khảo tài liệu Thanh ứng dụng trên cùng – Material Design 3 nếu cần.

Tuỳ chỉnh màu thanh trạng thái

Để ứng dụng hiện nội dung tràn viền, bạn có thể tuỳ chỉnh màu của thanh trạng thái cho phù hợp với màu nền.

  1. Trong Theme.kt, hãy thêm phương thức mới này để đổi màu thanh trạng thái và thanh điều hướng cho từng cạnh.
/**
 * 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. Trong hàm SuperheroesTheme(), hãy gọi hàm setUpEdgeToEdge() ngay trong khối 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. Lấy đoạn mã giải pháp

Để tải xuống mã cho lớp học lập trình đã kết thúc, bạn có thể sử dụng lệnh git này:

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

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp zip, sau đó giải nén và mở trong Android Studio.

Nếu bạn muốn xem mã giải pháp, hãy xem mã đó trên GitHub.