Thêm danh sách có thể cuộn

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

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách tạo danh sách có thể cuộn trong ứng dụng của mình bằng Jetpack Compose.

Bạn sẽ làm việc với ứng dụng Affirmations (Lời động viên), hiển thị một danh sách lời động viên kết hợp với hình ảnh đẹp để mang lại năng lượng tích cực cho ngày của bạn!

Dữ liệu đã có sẵn ở đó, bạn chỉ cần lấy và hiển thị dữ những dữ liệu này lên giao diện người dùng.

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

  • Quen thuộc với các danh sách trong Kotlin
  • Có kinh nghiệm xây dựng bố cục bằng Jetpack Compose
  • Trải nghiệm chạy các ứng dụng trên một thiết bị hoặc trình mô phỏng

Kiến thức bạn sẽ học được

  • Cách tạo thẻ Material Design bằng Jetpack Compose
  • Cách tạo danh sách có thể cuộn bằng Jetpack Compose

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

  • Bạn sẽ dùng một ứng dụng hiện có và thêm một danh sách có thể cuộn vào giao diện người dùng

Thành phẩm sẽ có dạng như sau:

286f5132aa155fa6.png

Bạn cần có

  • Một máy tính có quyền truy cập Internet, trình duyệt web và Android Studio
  • Quyền truy cập vào GitHub

Tải mã khởi đầu xuống

Trong Android Studio, hãy mở thư mục basic-android-kotlin-compose-training-affirmations.

Ứng dụng dự kiến sẽ hiển thị một màn hình trống khi được xây dựng từ mã nhánh starter.

3beea0789e2eeaba.png

2. Tạo một lớp dữ liệu mục trong danh sách

Tạo lớp dữ liệu cho một Affirmation (Lời động viên)

Trong ứng dụng Android, danh sách bao gồm các mục trong danh sách. Đối với các phần dữ liệu đơn lẻ, dữ liệu này có thể đơn giản như một chuỗi hoặc một số nguyên. Đối với những mục trong danh sách có nhiều phần dữ liệu, chẳng hạn như hình ảnh và văn bản, bạn sẽ cần một lớp chứa tất cả các thuộc tính này. Lớp dữ liệu là một loại lớp chỉ chứa các thuộc tính, có thể cung cấp một số phương thức tiện ích để xử lý những thuộc tính đó.

  1. Tạo một gói mới trong mục com.example.affirmations.

89c8d8485c685fac.png

Đặt tên cho mô hình gói mới. Gói mô hình sẽ chứa mô hình dữ liệu được biểu thị bằng một lớp dữ liệu. Lớp dữ liệu đó sẽ chứa các thuộc tính biểu thị thông tin liên quan đến nội dung sẽ trở thành một "Affirmation" (Lời động viên), bao gồm cả tài nguyên chuỗi và tài nguyên hình ảnh. Gói là thư mục chứa các lớp và thậm chí là những thư mục khác.

b54fb6bf57de44c8.png

  1. Tạo một lớp mới trong gói com.example.affirmations.model.

58510a651bd49100.png

Đặt tên cho lớp mới là Affirmation (Lời động viên) và đặt lớp đó là Data Class (Lớp dữ liệu).

7f94b65ee3d8407f.png

  1. Mỗi Affirmation bao gồm một hình ảnh và một chuỗi. Tạo hai thuộc tính val trong lớp dữ liệu Affirmation. Một thuộc tính sẽ được gọi là stringResourceId và thuộc tính còn lại là imageResourceId. Cả hai đều phải là số nguyên.

Affirmation.kt

data class Affirmation(
    val stringResourceId: Int,
    val imageResourceId: Int
)
  1. Chú thích thuộc tính stringResourceId bằng chú thích @StringRes và chú thích imageResourceId bằng chú thích @DrawableRes. stringResourceId biểu thị mã nhận dạng của văn bản lời động viên được lưu trữ trong một tài nguyên chuỗi. imageResourceId biểu thị mã nhận dạng của hình ảnh minh hoạ lời động viên được lưu trữ trong một tài nguyên có thể vẽ.

Affirmation.kt

import androidx.annotation.DrawableRes
import androidx.annotation.StringRes

data class Affirmation(
    @StringRes val stringResourceId: Int,
    @DrawableRes val imageResourceId: Int
)
  1. Trong gói com.example.affirmations.data, hãy mở tệp Datasource.kt rồi huỷ nhận xét về hai câu lệnh nhập và nội dung của lớp Datasource.

Datasource.kt

import com.example.affirmations.R
import com.example.affirmations.model.Affirmation

class Datasource() {
    fun loadAffirmations(): List<Affirmation> {
        return listOf<Affirmation>(
            Affirmation(R.string.affirmation1, R.drawable.image1),
            Affirmation(R.string.affirmation2, R.drawable.image2),
            Affirmation(R.string.affirmation3, R.drawable.image3),
            Affirmation(R.string.affirmation4, R.drawable.image4),
            Affirmation(R.string.affirmation5, R.drawable.image5),
            Affirmation(R.string.affirmation6, R.drawable.image6),
            Affirmation(R.string.affirmation7, R.drawable.image7),
            Affirmation(R.string.affirmation8, R.drawable.image8),
            Affirmation(R.string.affirmation9, R.drawable.image9),
            Affirmation(R.string.affirmation10, R.drawable.image10))
    }
}

3. Thêm danh sách vào ứng dụng

Tạo thẻ mục trong danh sách

Ứng dụng này nhằm hiển thị danh sách lời động viên. Bước đầu tiên trong việc định cấu hình giao diện người dùng để hiển thị danh sách là tạo một mục trong danh sách. Mỗi mục lời động viên bao gồm một hình ảnh và một chuỗi. Dữ liệu cho từng mục này đi kèm với mã khởi đầu và bạn sẽ tạo thành phần giao diện người dùng để hiển thị mục đó.

Mục này sẽ bao gồm một thành phần kết hợp Card, chứa Image và một thành phần kết hợp Text. Trong Compose, Card là một nền tảng hiển thị nội dung và các hành động trong một vùng chứa. Thẻ Affirmation (Lời động viên) sẽ có dạng như sau trong bản xem trước:

4f657540712a069f.png

Thẻ này cho thấy một hình ảnh kèm theo văn bản nào đó ở bên dưới. Bạn có thể tạo được bố cục dọc này bằng cách dùng thành phần kết hợp Column gói trong một thành phần kết hợp Card. Bạn có thể tự mình thử hoặc làm theo các bước dưới đây để tạo được bố cục này.

  1. Mở tệp MainActivity.kt.
  2. Tạo một phương thức mới bên dưới phương thức AffirmationsApp() (được gọi là AffirmationCard()) rồi chú thích phương thức đó bằng chú thích @Composable.

MainActivity.kt

@Composable
fun AffirmationsApp() {
}

@Composable
fun AffirmationCard() {

}
  1. Chỉnh sửa chữ ký của phương thức để lấy đối tượng Affirmation làm thông số. Đối tượng Affirmation đến từ gói model.

MainActivity.kt

import com.example.affirmations.model.Affirmation

@Composable
fun AffirmationCard(affirmation: Affirmation) {

}
  1. Thêm một tham số modifier vào chữ ký. Đặt giá trị mặc định là Modifier cho tham số này.

MainActivity.kt

@Composable
fun AffirmationCard(affirmation: Affirmation, modifier: Modifier = Modifier) {

}
  1. Bên trong phương thức AffirmationCard, hãy gọi thành phần kết hợp Card. Truyền vào tham số modifier.

MainActivity.kt

import androidx.compose.material3.Card

@Composable
fun AffirmationCard(affirmation: Affirmation, modifier: Modifier = Modifier) {
    Card(modifier = modifier) {

    }
}
  1. Thêm một thành phần kết hợp Column bên trong thành phần kết hợp Card. Các mục trong thành phần kết hợp Column tự sắp xếp theo chiều dọc trong giao diện người dùng. Điều này cho phép bạn đặt hình ảnh lên phía trên văn bản liên kết. Ngược lại, thành phần kết hợp Row sắp xếp các mục bên trong theo chiều ngang.

MainActivity.kt

import androidx.compose.foundation.layout.Column

@Composable
fun AffirmationCard(affirmation: Affirmation, modifier: Modifier = Modifier) {
    Card(modifier = modifier) {
        Column {

        }
    }

}
  1. Thêm một thành phần kết hợp Image bên trong phần nội dung lambda của thành phần kết hợp Column. Hãy nhớ là thành phần kết hợp Image luôn yêu cầu hiển thị tài nguyên và contentDescription. Tài nguyên phải là painterResource được truyền vào tham số painter. Phương thức painterResource sẽ tải các vectơ vẽ được hoặc định dạng thành phần tạo từ đường quét như PNG. Ngoài ra, hãy truyền stringResource cho thông số contentDescription.

MainActivity.kt

import androidx.compose.foundation.Image
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource

@Composable
fun AffirmationCard(affirmation: Affirmation, modifier: Modifier = Modifier) {
    Card(modifier = modifier) {
        Column {
            Image(
                painter = painterResource(affirmation.imageResourceId),
                contentDescription = stringResource(affirmation.stringResourceId),
            )
        }
    }
}
  1. Ngoài các thông số paintercontentDescription, hãy truyền modifiercontentScale. contentScale xác định cách hình ảnh hiển thị và thay đổi kích thước theo tỷ lệ. Đặt thuộc tính fillMaxWidth và chiều cao 194.dp cho đối tượng Modifier. contentScale phải là ContentScale.Crop.

MainActivity.kt

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.ui.unit.dp
import androidx.compose.ui.layout.ContentScale

@Composable
fun AffirmationCard(affirmation: Affirmation, modifier: Modifier = Modifier) {
    Card(modifier = modifier) {
        Column {
            Image(
                painter = painterResource(affirmation.imageResourceId),
                contentDescription = stringResource(affirmation.stringResourceId),
                modifier = Modifier
                    .fillMaxWidth()
                    .height(194.dp),
                contentScale = ContentScale.Crop
            )
        }
    }
}
  1. Bên trong Column, hãy tạo một thành phần kết hợp Text sau thành phần kết hợp Image. Truyền stringResource của affirmation.stringResourceId vào tham số text, truyền đối tượng Modifier với thuộc tính padding được đặt thành 16.dp và đặt giao diện văn bản bằng cách truyền MaterialTheme.typography.headlineSmall vào tham số style.

MainActivity.kt

import androidx.compose.material3.Text
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.platform.LocalContext

@Composable
fun AffirmationCard(affirmation: Affirmation, modifier: Modifier = Modifier) {
    Card(modifier = modifier) {
        Column {
            Image(
                painter = painterResource(affirmation.imageResourceId),
                contentDescription = stringResource(affirmation.stringResourceId),
                modifier = Modifier
                    .fillMaxWidth()
                    .height(194.dp),
                contentScale = ContentScale.Crop
            )
            Text(
                text = LocalContext.current.getString(affirmation.stringResourceId),
                modifier = Modifier.padding(16.dp),
                style = MaterialTheme.typography.headlineSmall
            )
        }
    }
}

Xem trước thành phần kết hợp AffirmationCard

Thẻ này là thành phần chính của giao diện người dùng cho ứng dụng Affirmations và bạn đã nỗ lực tạo ra thẻ đó! Để kiểm tra xem thẻ có chính xác hay không, bạn có thể tạo một thành phần kết hợp có thể xem trước mà không cần khởi chạy toàn bộ ứng dụng.

  1. Tạo một phương thức riêng tư có tên là AffirmationCardPreview(). Chú thích phương thức bằng @Preview@Composable.

MainActivity.kt

import androidx.compose.ui.tooling.preview.Preview

@Preview
@Composable
private fun AffirmationCardPreview() {

}
  1. Bên trong phương thức đó, hãy gọi thành phần kết hợp AffirmationCard rồi truyền vào thành phần kết hợp này một đối tượng Affirmation mới, đồng thời truyền tài nguyên chuỗi R.string.affirmation1 và tài nguyên có thể vẽ R.drawable.image1 vào hàm khởi tạo của thành phần kết hợp nêu trên.

MainActivity.kt

@Preview
@Composable
private fun AffirmationCardPreview() {
    AffirmationCard(Affirmation(R.string.affirmation1, R.drawable.image1))
}
  1. Mở thẻ Split (Tách) để thấy bản xem trước của AffirmationCard. Nếu cần, hãy nhấp vào Build & Refresh (Tạo và làm mới) trong ngăn Design (Thiết kế) để hiển thị bản xem trước.

924a4df2c1db236c.png

Tạo danh sách

Thành phần của mục trong danh sách là yếu tố nền tảng của danh sách. Sau khi tạo xong mục trong danh sách, bạn có thể tận dụng mục đó để tạo thành phần danh sách.

  1. Tạo một hàm có tên là AffirmationList(), chú thích bằng chú thích @Composable và khai báo List của đối tượng Affirmation dưới dạng tham số trong chữ ký của phương thức.

MainActivity.kt

@Composable
fun AffirmationList(affirmationList: List<Affirmation>) {

}
  1. Khai báo một đối tượng modifier dưới dạng tham số trong chữ ký phương thức có giá trị mặc định là Modifier.

MainActivity.kt

@Composable
fun AffirmationList(affirmationList: List<Affirmation>, modifier: Modifier = Modifier) {

}
  1. Trong Jetpack Compose, bạn có thể tạo một danh sách có thể cuộn bằng cách sử dụng thành phần kết hợp LazyColumn. Sự khác biệt giữa LazyColumnColumnColumn phải được dùng khi bạn có ít các mục để hiển thị, vì Compose sẽ tải tất cả các mục này cùng một lúc. Column chỉ có thể giữ một số thành phần kết hợp được xác định trước hoặc cố định. LazyColumn có thể thêm nội dung theo yêu cầu, phù hợp với danh sách dài, đặc biệt khi không xác định được độ dài của danh sách. Theo mặc định, LazyColumn cũng cung cấp chức năng cuộn mà không cần thêm mã. Khai báo một thành phần kết hợp LazyColumn bên trong hàm AffirmationList(). Truyền đối tượng modifier làm đối số cho LazyColumn.

MainActivity.kt

import androidx.compose.foundation.lazy.LazyColumn

@Composable
fun AffirmationList(affirmationList: List<Affirmation>, modifier: Modifier = Modifier) {
    LazyColumn(modifier = modifier) {

    }
}
  1. Trong phần nội dung hàm lambda của LazyColumn, hãy gọi phương thức items() rồi truyền vào affirmationList. Phương thức items() là cách bạn thêm các mục vào LazyColumn. Phương thức này có phần khác biệt đối với thành phần kết hợp này và không phải là phương pháp phổ biến đối với hầu hết các thành phần kết hợp.

MainActivity.kt

import androidx.compose.foundation.lazy.items

@Composable
fun AffirmationList(affirmationList: List<Affirmation>, modifier: Modifier = Modifier) {
    LazyColumn(modifier = modifier) {
        items(affirmationList) {

        }
    }
}
  1. Lệnh gọi đến phương thức items() yêu cầu hàm lambda. Trong hàm đó, hãy chỉ định tham số affirmation đại diện cho một mục lời tự động viên qua affirmationList.

MainActivity.kt

@Composable
fun AffirmationList(affirmationList: List<Affirmation>, modifier: Modifier = Modifier) {
    LazyColumn(modifier = modifier) {
        items(affirmationList) { affirmation ->

        }
    }
}
  1. Đối với mỗi lời động viên trong danh sách, hãy gọi thành phần kết hợp AffirmationCard(). Truyền vào đó đối tượng affirmationModifier có thuộc tính padding được đặt thành 8.dp.

MainActivity.kt

@Composable
fun AffirmationList(affirmationList: List<Affirmation>, modifier: Modifier = Modifier) {
    LazyColumn(modifier = modifier) {
        items(affirmationList) { affirmation ->
            AffirmationCard(
                affirmation = affirmation,
                modifier = Modifier.padding(8.dp)
            )
        }
    }
}

Hiển thị danh sách

  1. Trong thành phần kết hợp AffirmationsApp, hãy truy xuất các hướng bố cục hiện tại rồi lưu vào một biến. Những hướng này sẽ được dùng để định cấu hình khoảng đệm sau này.

MainActivity.kt

import com.example.affirmations.data.Datasource

@Composable
fun AffirmationsApp() {
    val layoutDirection = LocalLayoutDirection.current
}
  1. Bây giờ, hãy tạo một thành phần kết hợp Surface. Thành phần kết hợp này sẽ đặt khoảng đệm cho thành phần kết hợp AffirmationsList.

MainActivity.kt

import com.example.affirmations.data.Datasource

@Composable
fun AffirmationsApp() {
    val layoutDirection = LocalLayoutDirection.current
    Surface() {
    }
}
  1. Truyền một Modifier vào thành phần kết hợp Surface lấp đầy chiều rộng và chiều cao tối đa của phần tử cha, đặt khoảng đệm cho thanh trạng thái, đồng thời đặt khoảng đệm bắt đầu và kết thúc cho layoutDirection. Sau đây là ví dụ về cách chuyển đổi đối tượng LayoutDirection thành khoảng đệm: WindowInsets.safeDrawing.asPaddingValues().calculateStartPadding(layoutDirection).

MainActivity.kt

import com.example.affirmations.data.Datasource

@Composable
fun AffirmationsApp() {
    val layoutDirection = LocalLayoutDirection.current
    Surface(
        Modifier = Modifier
        .fillMaxSize()
        .statusBarsPadding()
        .padding(
            start = WindowInsets.safeDrawing.asPaddingValues()
                    .calculateStartPadding(layoutDirection),
            end = WindowInsets.safeDrawing.asPaddingValues()
                    .calculateEndPadding(layoutDirection),
        ),
    ) {
    }
}
  1. Trong hàm lambda cho thành phần kết hợp Surface, hãy gọi thành phần kết hợp AffirmationList và truyền DataSource().loadAffirmations() vào tham số affirmationList.

MainActivity.kt

import com.example.affirmations.data.Datasource

@Composable
fun AffirmationsApp() {
    val layoutDirection = LocalLayoutDirection.current
    Surface(
        Modifier = Modifier
        .fillMaxSize()
        .statusBarsPadding()
        .padding(
            start = WindowInsets.safeDrawing.asPaddingValues()
                    .calculateStartPadding(layoutDirection),
            end = WindowInsets.safeDrawing.asPaddingValues()
                    .calculateEndPadding(layoutDirection),
        ),
    ) {
        AffirmationsList(
            affirmationList = Datasource().loadAffirmations(),
        )
    }
}

Chạy ứng dụng Affirmations trên một thiết bị hoặc trình mô phỏng rồi xem sản phẩm hoàn thiện!

286f5132aa155fa6.png

4. Lấy mã giải pháp

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

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

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

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

5. Kết luận

Giờ đây, bạn đã biết cách tạo thẻ, mục trong danh sách và danh sách có thể cuộn bằng Jetpack Compose! Xin lưu ý rằng đây chỉ là những công cụ cơ bản để tạo danh sách. Bạn có thể thoả sức sáng tạo và tuỳ chỉnh các mục trong danh sách theo ý muốn!

Tóm tắt

  • Sử dụng thành phần kết hợp Card để tạo mục trong danh sách.
  • Sửa đổi giao diện người dùng nằm trong thành phần kết hợp Card.
  • Tạo một danh sách có thể cuộn bằng cách sử dụng thành phần kết hợp LazyColumn.
  • Tạo danh sách bằng cách sử dụng các mục trong danh sách tuỳ chỉnh.