1. Giới thiệu
Trong lớp học lập trình này, bạn sẽ tìm hiểu các khái niệm nâng cao liên quan đến các API Trạng thái và API Hiệu ứng phụ trongJetpack Compose. Chúng ta sẽ tìm hiểu cách tạo trình giữ trạng thái cho thành phần kết hợp có tính trạng thái với logic đáng kể, cách tạo coroutine và gọi các hàm có thể tạm ngưng (suspend function) bằng đoạn mã trong Compose cũng như cách kích hoạt hiệu ứng phụ để thực hiện các trường hợp sử dụng khác nhau.
Kiến thức bạn sẽ học được
- Cách quan sát các luồng dữ liệu của đoạn mã Compose để cập nhật giao diện người dùng.
- Cách tạo trình giữ trạng thái cho thành phần kết hợp có tính trạng thái.
- Các API hiệu ứng phụ như
LaunchedEffect
,rememberUpdatedState
,DisposableEffect
,produceState
vàderivedStateOf
. - Cách tạo coroutine và gọi các hàm có thể tạm ngưng trong thành phần kết hợp bằng API
rememberCoroutineScope
.
Bạn cần có
- Phiên bản Android Studio mới nhất
- Kinh nghiệm về cú pháp Kotlin, bao gồm cả lambda.
- Kinh nghiệm cơ bản về Compose Hãy cân nhắc việc tham gia lớp học lập trình cơ bản về Jetpack Compose trước khi tham gia lớp học lập trình này.
- Các khái niệm cơ bản về trạng thái trong Compose như Luồng dữ liệu đơn hướng (UDF), ViewModels, chuyển trạng thái lên trên (state hoisting), thành phần kết hợp phi trạng thái/có tính trạng thái, API ô trống cũng như các API trạng thái
remember
vàmutableStateOf
. Để nắm được kiến thức này, hãy cân nhắc đọc tài liệu về Trạng thái và Jetpack Compose hoặc hoàn thành lớp học lập trình Sử dụng Trạng thái trong Jetpack Compose. - Kiến thức cơ bản về coroutine trong Kotlin.
- Hiểu biết cơ bản về vòng đời của thành phần kết hợp.
Sản phẩm bạn sẽ tạo ra
Trong lớp học lập trình này, chúng ta sẽ bắt đầu với một ứng dụng chưa hoàn thiện, đó là ứng dụng Crane – nghiên cứu của Material. Chúng ta sẽ thêm một số tính năng để cải thiện ứng dụng này.
2. Thiết lập
Lấy mã
Bạn có thể tìm thấy đoạn mã dành cho lớp học lập trình này trong android-compose-codelabs trên kho lưu trữ GitHub. Để sao chép, hãy chạy:
$ git clone https://github.com/googlecodelabs/android-compose-codelabs
Ngoài ra, bạn có thể tải kho lưu trữ ở dạng định dạng tệp zip:
Xem xét ứng dụng mẫu
Đoạn mã bạn vừa tải xuống có chứa mã dành cho tất cả lớp học lập trình Compose hiện có. Để hoàn tất lớp học lập trình này, hãy mở dự án AdvancedStateAndSideEffectsCodelab
trong Android Studio Arctic Fox.
Bạn bên bắt đầu với các đoạn mã trong nhánh main và làm theo hướng dẫn từng bước của lớp học lập trình theo tốc độ của bản thân.
Xuyên suốt lớp học lập trình, bạn sẽ thấy các đoạn mã bạn cần thêm vào dự án. Có lúc, bạn cũng sẽ cần phải xoá những dòng mã được đề cập rõ ràng trong các nhận xét trong đoạn mã.
Làm quen với các đoạn mã và chạy ứng dụng mẫu
Hãy dành chút thời gian tìm hiểu cấu trúc dự án và chạy ứng dụng.
Khi chạy ứng dụng từ nhánh main, bạn sẽ thấy rằng một số chức năng như ngăn hoặc tải điểm đến của chuyến bay không hoạt động! Đó là những gì chúng ta sẽ thực hiện trong các bước tiếp theo của lớp học lập trình.
Kiểm thử giao diện người dùng
Ứng dụng này sử dụng các bài kiểm thử giao diện người dùng rất cơ bản có trong thư mục androidTest
. Cả 2 nhánh main
và end
sẽ luôn vượt qua các bài kiểm tra này.
[Không bắt buộc] Hiển thị bản đồ trên màn hình chi tiết
Bạn không cần phải làm theo các bước để hiển thị bản đồ thành phố trên màn hình chi tiết. Tuy nhiên, nếu muốn làm như vậy, bạn cần phải có khóa API cá nhân như được nêu trong tài liệu Maps (Bản đồ). Thêm khóa đó vào tệp local.properties
như sau:
// local.properties file
google.maps.key={insert_your_api_key_here}
Giải pháp cho lớp học lập trình
Để nhận nhánh end
bằng git, hãy dùng lệnh sau:
$ git clone -b end https://github.com/googlecodelabs/android-compose-codelabs
Hoặc bạn có thể tải đoạn mã chứa giải pháp từ đây:
Câu hỏi thường gặp
3. Quy trình tạo trạng thái giao diện người dùng
Như bạn thấy khi chạy ứng dụng từ nhánh main
, danh sách các điểm đến của chuyến bay bị trống!
Để khắc phục vấn đề này, bạn phải hoàn tất 2 bước sau đây:
- Thêm logic trong
ViewModel
để tạo trạng thái giao diện người dùng. Trong trường hợp của bạn, đây là danh sách điểm đến được đề xuất. - Sử dụng trạng thái giao diện người dùng từ giao diện người dùng. Trạng thái này sẽ hiển thị giao diện người dùng trên màn hình.
Trong phần này, bạn sẽ hoàn tất bước đầu tiên.
Một cấu trúc tối ưu của ứng dụng được sắp xếp theo lớp để tuân theo các phương pháp cơ bản về thiết kế một hệ thống tối ưu, chẳng hạn như phân tách các mối quan ngại và khả năng kiểm thử.
Phần Tạo trạng thái giao diện người dùng đề cập đến quá trình ứng dụng truy cập vào lớp dữ liệu, áp dụng quy tắc kinh doanh nếu cần và hiển thị trạng thái giao diện người dùng được sử dụng từ giao diện người dùng.
Lớp dữ liệu trong ứng dụng này đã được triển khai. Bây giờ, bạn sẽ tạo trạng thái (danh sách điểm đến được đề xuất) để giao diện người dùng có thể sử dụng trạng thái đó.
Bạn có thể dùng một số API để tạo trạng thái giao diện người dùng. Các phương án thay thế được tóm tắt trong tài liệu về Các loại đầu ra trong quy trình tạo trạng thái. Nhìn chung, bạn nên sử dụng StateFlow
của Kotlin để tạo trạng thái giao diện người dùng.
Để tạo trạng thái giao diện người dùng, hãy làm theo các bước sau đây:
- Mở
home/MainViewModel.kt
. - Xác định biến
_suggestedDestinations
riêng tư thuộc loạiMutableStateFlow
để biểu thị danh sách điểm đến được đề xuất và đặt một danh sách trống làm giá trị bắt đầu.
private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList())
- Xác định biến không thể thay đổi thứ hai
suggestedDestinations
thuộc loạiStateFlow
. Đây là biến chỉ đọc công khai có thể được sử dụng từ giao diện người dùng. Việc hiển thị biến chỉ đọc trong khi sử dụng biến có thể thay đổi trong nội bộ là một phương pháp hay. Bằng cách này, bạn đảm bảo rằng chỉ có thể sửa đổi trạng thái giao diện người dùng thông quaViewModel
, giúp lớp này trở thành nguồn đáng tin cậy duy nhất. Hàm mở rộngasStateFlow
chuyển đổi luồng từ có thể thay đổi thành không thể thay đổi.
private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList())
val suggestedDestinations: StateFlow<List<ExploreModel>> = _suggestedDestinations.asStateFlow()
- Trong khối init của
ViewModel
, hãy thêm một lệnh gọi từdestinationsRepository
để nhận các điểm đến từ lớp dữ liệu.
private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList())
val suggestedDestinations: StateFlow<List<ExploreModel>> = _suggestedDestinations.asStateFlow()
init {
_suggestedDestinations.value = destinationsRepository.destinations
}
- Cuối cùng, hãy huỷ nhận xét về việc sử dụng biến nội bộ
_suggestedDestinations
mà bạn thấy trong lớp này để có thể cập nhật biến này đúng cách với các sự kiện đến từ giao diện người dùng.
Vậy là xong! Bạn đã hoàn thành bước đầu tiên! ViewModel
hiện có thể tạo trạng thái giao diện người dùng. Trong bước tiếp theo, bạn sẽ sử dụng trạng thái này từ giao diện người dùng.
4. Sử dụng luồng dữ liệu từ ViewModel một cách an toàn
Danh sách điểm đến của chuyến bay vẫn trống. Ở bước trước, bạn đã tạo trạng thái giao diện người dùng trong MainViewModel
. Giờ đây, bạn sẽ sử dụng trạng thái giao diện người dùng có trong MainViewModel
để hiển thị trong giao diện người dùng.
Mở lại tệp home/CraneHome.kt
và xem thành phần kết hợp CraneHomeContent
.
Có một ghi chú trong mục TODO (Việc cần làm) phía trên định nghĩa về suggestedDestinations
được gán cho danh sách trống được ghi nhớ. Đây là nội dung sẽ hiển thị trên màn hình: một danh sách trống! Trong bước này, chúng ta sẽ khắc phục vấn đề đó và trình bày các đích đến đề xuất mà MainViewModel
hiển thị.
Mở home/MainViewModel.kt
để xem StateFlow suggestedDestinations
, vốn được khởi tạo cho destinationsRepository.destinations
và sẽ cập nhật khi các hàm updatePeople
hoặc toDestinationChanged
được gọi.
Bạn muốn giao diện người dùng trong thành phần kết hợp CraneHomeContent
có thể cập nhật bất cứ khi nào có một mục mới được xuất vào luồng dữ liệu suggestedDestinations
. Bạn có thể sử dụng hàm collectAsStateWithLifecycle()
. collectAsStateWithLifecycle()
thu thập các giá trị từ StateFlow
và biểu thị giá trị mới nhất thông qua API State (Trạng thái) của Compose. Điều này sẽ khiến mã Compose đọc giá trị trạng thái đó kết hợp lại khi có một mục mới được xuất vào luồng dữ liệu.
Để bắt đầu sử dụng API collectAsStateWithLifecycle
, trước tiên hãy thêm phần phụ thuộc sau đây vào app/build.gradle
. Biến lifecycle_version
đã được xác định trong dự án bằng phiên bản phù hợp.
dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version"
}
Quay lại thành phần kết hợp CraneHomeContent
và thay thế dòng chỉ định suggestedDestinations
bằng lệnh gọi tới collectAsStateWithLifecycle
trên thuộc tính suggestedDestinations
của ViewModel
:
import androidx.compose.runtime.collectAsState
@Composable
fun CraneHomeContent(
onExploreItemClicked: OnExploreItemClicked,
openDrawer: () -> Unit,
modifier: Modifier = Modifier,
viewModel: MainViewModel = viewModel(),
) {
val suggestedDestinations by viewModel.suggestedDestinations.collectAsStateWithLifecycle()
// ...
}
Nếu chạy ứng dụng, bạn sẽ thấy danh sách các điểm đến được điền sẵn và danh sách này sẽ thay đổi bất cứ khi nào bạn nhấn vào số hành khách.
5. LaunchedEffect và rememberUpdatedState
Trong dự án, hiện có một tệp home/LandingScreen.kt
chưa được dùng. Chúng ta phải thêm màn hình đích vào ứng dụng. Màn hình này có thể được dùng để tải ngầm tất cả dữ liệu cần thiết.
Màn hình đích sẽ chiếm toàn bộ màn hình và hiển thị biểu trưng của ứng dụng ở giữa màn hình. Tốt nhất là chúng ta nên hiển thị màn hình đích, sau khi toàn bộ dữ liệu đã được tải xong, chúng ta sẽ thông báo cho phương thức gọi rằng có thể bỏ qua màn hình đích bằng cách sử dụng lệnh gọi lại onTimeout
.
Bạn nên sử dụng coroutine Kotlin để thực hiện các thao tác không đồng bộ trên Android. Một ứng dụng thường sẽ sử dụng coroutine để tải ngầm mọi thứ khi ứng dụng khởi động. Jetpack Compose cung cấp các API giúp sử dụng coroutine an toàn trong lớp giao diện người dùng. Do ứng dụng này không giao tiếp với một phần phụ trợ (backend), chúng ta sẽ dùng hàm delay
của coroutine để mô phỏng việc tải ngầm các nội dung.
Hiệu ứng phụ trong Compose là sự thay đổi về trạng thái của ứng dụng bên ngoài phạm vi một hàm kết hợp (composable). Việc thay đổi trạng thái để hiển thị/ẩn màn hình đích sẽ xảy ra trong lệnh gọi lại onTimeout
và vì trước khi gọi onTimeout
, chúng ta cần tải mọi thứ bằng coroutine, nên việc thay đổi trạng thái cần diễn ra trong ngữ cảnh coroutine!
Để gọi các chức năng tạm ngưng một cách an toàn từ bên trong thành phần kết hợp (composable), hãy sử dụng API LaunchedEffect
để kích hoạt hiệu ứng phụ trong phạm vi coroutine trong ứng dụng Compose.
Khi LaunchedEffect
nhập một Thành phần, công cụ này sẽ khởi chạy một coroutine với khối mã được truyền dưới dạng tham số. Coroutine sẽ bị huỷ nếu LaunchedEffect
thoát khỏi thành phần đó.
Mặc dù mã tiếp theo không đúng, nhưng hãy tìm hiểu cách sử dụng API này và thảo luận về lý do khiến mã sau không đúng. Chúng tôi sẽ gọi thành phần kết hợp LandingScreen
sau trong bước này.
// home/LandingScreen.kt file
import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay
@Composable
fun LandingScreen(onTimeout: () -> Unit, modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
// Start a side effect to load things in the background
// and call onTimeout() when finished.
// Passing onTimeout as a parameter to LaunchedEffect
// is wrong! Don't do this. We'll improve this code in a sec.
LaunchedEffect(onTimeout) {
delay(SplashWaitTime) // Simulates loading things
onTimeout()
}
Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
}
}
Một số API có hiệu ứng phụ như LaunchedEffect
sẽ lấy một số lượng biến khoá làm tham số dùng để khởi động lại hiệu ứng mỗi khi một trong các khoá đó thay đổi. Bạn đã phát hiện thấy lỗi? Chúng tôi không muốn khởi động lại LaunchedEffect
nếu phương thức gọi đến hàm có khả năng kết hợp này chuyển giá trị lambda onTimeout
khác. Điều đó sẽ khiến delay
bắt đầu lại và chúng tôi sẽ không đáp ứng các yêu cầu của mình.
Để chỉ kích hoạt hiệu ứng phụ một lần trong vòng đời của thành phần kết hợp này, sử dụng hằng số làm khoá, ví dụ: LaunchedEffect(true) { ... }
. Tuy nhiên, hiện chúng ta sẽ không bảo vệ các thay đổi đối với onTimeout
!
Nếu onTimeout
thay đổi trong khi hiệu ứng phụ đang diễn ra, không có gì đảm bảo onTimeout
cuối cùng sẽ được gọi khi hiệu ứng kết thúc. Để đảm bảo điều này bằng cách thu thập và cập nhật giá trị mới, sử dụng API rememberUpdatedState
:
// home/LandingScreen.kt file
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import kotlinx.coroutines.delay
@Composable
fun LandingScreen(onTimeout: () -> Unit, modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
// This will always refer to the latest onTimeout function that
// LandingScreen was recomposed with
val currentOnTimeout by rememberUpdatedState(onTimeout)
// Create an effect that matches the lifecycle of LandingScreen.
// If LandingScreen recomposes or onTimeout changes,
// the delay shouldn't start again.
LaunchedEffect(true) {
delay(SplashWaitTime)
currentOnTimeout()
}
Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
}
}
Hiển thị màn hình đích
Bây giờ, chúng ta cần hiển thị màn hình đích khi ứng dụng được mở. Mở tệp home/MainActivity.kt
và xem thành phần kết hợp (composable) MainScreen
được gọi đầu tiên.
Trong thành phần kết hợp (composable) MainScreen
, chúng ta chỉ cần thêm trạng thái nội bộ theo dõi liệu trang đích có hiển thị hay không:
// home/MainActivity.kt file
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
private fun MainScreen(onExploreItemClicked: OnExploreItemClicked) {
Surface(color = MaterialTheme.colors.primary) {
var showLandingScreen by remember { mutableStateOf(true) }
if (showLandingScreen) {
LandingScreen(onTimeout = { showLandingScreen = false })
} else {
CraneHome(onExploreItemClicked = onExploreItemClicked)
}
}
}
Nếu bạn chạy ứng dụng ngay bây giờ, bạn sẽ thấy LandingScreen
xuất hiện và biến mất sau 2 giây.
6. rememberCoroutineScope
Ở bước này, chúng ta sẽ khiến ngăn điều hướng hoạt động. Hiện tại, sẽ không có gì xảy ra nếu bạn nhấn vào trình đơn ba đường kẻ.
Mở tệp home/CraneHome.kt
và xem thành phần kết hợp (composable) CraneHome
để biết chúng ta cần mở ngăn điều hướng ở đâu: trong lệnh gọi lại openDrawer
!
Trong CraneHome
, chúng ta có một scaffoldState
chứa DrawerState
. DrawerState
có các phương thức để mở và đóng ngăn điều hướng theo phương thức lập trình. Tuy nhiên, nếu bạn muốn ghi scaffoldState.drawerState.open()
trong lệnh gọi lại openDrawer
, bạn sẽ thấy một lỗi xuất hiện! Nguyên nhân vì hàm open
là hàm tạm ngưng. Chúng ta một lần nữa ở trong coroutine.
Ngoài API để giúp coroutine gọi an toàn từ lớp giao diện người dùng, một số API Compose là chức năng tạm ngưng. Một ví dụ cho trường hợp này là API để mở ngăn điều hướng. Các hàm tạm ngưng, ngoài việc có thể chạy mã không đồng bộ còn giúp trình bày các ý tưởng xảy ra theo thời gian. Do việc mở ngăn đòi hỏi thời gian, nên chuyển động và ảnh động tiềm năng cần được phản ánh hoàn hảo bằng chức năng tạm ngưng. Thao tác này sẽ tạm ngừng việc thực thi coroutine ở vị trí nó được gọi cho đến khi kết thúc và tiếp tục thực thi.
Phải gọi scaffoldState.drawerState.open()
trong coroutine. Chúng ta có thể làm gì? openDrawer
là hàm gọi lại đơn giản, do đó:
- Chúng ta không thể gọi các hàm tạm ngưng trong đó bởi
openDrawer
không được thực thi trong bối cảnh một coroutine. - Chúng ta không thể sử dụng
LaunchedEffect
như trước vì chúng ta không thể gọi thành phần kết hợp (composable) trongopenDrawer
. Chúng ta không ở trong Composition.
Nếu muốn khởi động một coroutine, chúng ta nên dùng phạm vi nào? Lý tưởng nhất là nên có CoroutineScope
tuân theo vòng đời của trang web cuộc gọi. Để làm điều này, hãy sử dụng API rememberCoroutineScope
. Phạm vi sẽ tự động bị hủy sau khi thoát khỏi Composition. Trong phạm vi đó, bạn có thể khởi động coroutine khi không ở trong Composition, chẳng hạn như trong lệnh gọi lại openDrawer
.
// home/CraneHome.kt file
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch
@Composable
fun CraneHome(
onExploreItemClicked: OnExploreItemClicked,
modifier: Modifier = Modifier,
) {
val scaffoldState = rememberScaffoldState()
Scaffold(
scaffoldState = scaffoldState,
modifier = Modifier.statusBarsPadding(),
drawerContent = {
CraneDrawer()
}
) {
val scope = rememberCoroutineScope()
CraneHomeContent(
modifier = modifier,
onExploreItemClicked = onExploreItemClicked,
openDrawer = {
scope.launch {
scaffoldState.drawerState.open()
}
}
)
}
}
Nếu chạy ứng dụng này, bạn sẽ thấy ngăn điều hướng mở ra khi nhấn vào biểu tượng trình đơn ba đường kẻ.
LaunchedEffect so với rememberCoroutineScope
Chúng ta không thể sử dụng LaunchedEffect
trong trường hợp này vì chúng ta cần kích hoạt cuộc gọi để tạo một coroutine trong lệnh gọi lại thông thường nằm ngoài Composition.
Xem lại bước trên màn hình đích đã dùng LaunchedEffect
, bạn có thể sử dụng rememberCoroutineScope
và gọi scope.launch { delay(); onTimeout(); }
thay vì sử dụng LaunchedEffect
không?
Bạn có thể làm vậy và dường như cách này có thể hiệu quả nhưng nó không đúng. Như đã giải thích trong tài liệu về Thinking in Compose (Tư duy trong Compose), bạn có thể gọi thành phần kết hợp (composable) bất cứ lúc nào. LaunchedEffect
đảm bảo rằng hiệu ứng phụ này sẽ được thực thi khi lệnh gọi thành phần kết hợp (composable) khiến nó nằm trong Composition. Nếu bạn sử dụng rememberCoroutineScope
và scope.launch
trong phần thân của LandingScreen
, thì coroutine sẽ được thực thi mỗi khi LandingScreen
được gọi bằng Compose bất kể lệnh gọi đó có khiến nó nằm trong Composition hay không. Do đó, bạn sẽ lãng phí tài nguyên và sẽ không thực thi hiệu ứng phụ này trong môi trường được kiểm soát.
7. Tạo phần tử giữ trạng thái
Bạn có thấy rằng nếu nhấn vào Choose Destination (Chọn điểm đến), bạn có thể chỉnh sửa trường này và lọc các thành phố dựa trên nội dung tìm kiếm đã nhập không? Bạn cũng có thể nhận thấy bất cứ khi nào bạn sửa đổi Choose Destination (Chọn điểm đến), kiểu văn bản sẽ thay đổi.
Mở tệp base/EditableUserInput.kt
. Thành phần kết hợp trạng thái CraneEditableUserInput
nhận được một vài tham số như hint
và caption
tương ứng với văn bản không bắt buộc bên cạnh biểu tượng. Ví dụ: caption
To (Tới) xuất hiện khi bạn tìm kiếm một điểm đến.
// base/EditableUserInput.kt file - code in the main branch
@Composable
fun CraneEditableUserInput(
hint: String,
caption: String? = null,
@DrawableRes vectorImageId: Int? = null,
onInputChanged: (String) -> Unit
) {
// TODO Codelab: Encapsulate this state in a state holder
var textState by remember { mutableStateOf(hint) }
val isHint = { textState == hint }
...
}
Tại sao?
Logic để cập nhật textState
và xác định xem nội dung đã hiển thị có tương ứng với gợi ý hay không đều nằm ở phần thân của thành phần kết hợp (composable) CraneEditableUserInput
. Điều này cũng gây ra một số nhược điểm:
- Giá trị của
TextField
không được nâng nên không thể bị kiểm soát từ bên ngoài, điều này khiến việc kiểm thử khó khăn hơn. - Logic của thành phần kết hợp (composable) này có thể phức tạp hơn và trạng thái nội bộ có thể thoát đồng bộ dễ dàng hơn.
Bằng cách tạo trình giữ trạng thái chịu trách nhiệm về trạng thái nội bộ của thành phần kết hợp (composable) này, bạn có thể tập trung tất cả các thay đổi về trạng thái ở cùng một nơi. Với cách này, trạng thái khó bị thoát đồng bộ hơn và logic liên quan sẽ được nhóm lại với nhau trong một lớp duy nhất. Hơn nữa, trạng thái này có thể dễ dàng được nâng lên và có thể được sử dụng từ người gọi của thành phần kết hợp (composable) này.
Trong trường hợp đó, bạn nên nâng trạng thái này do đây là thành phần giao diện người dùng cấp thấp có thể được sử dụng lại trong các phần khác của ứng dụng. Do đó, càng linh hoạt và có thể kiểm soát nhiều thì càng tốt.
Tạo trình giữ trạng thái
Do CraneEditableUserInput
là thành phần có thể sử dụng lại, hãy tạo một lớp thông thường làm trình giữ trạng thái có tên EditableUserInputState
trong cùng tệp như sau:
// base/EditableUserInput.kt file
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
class EditableUserInputState(private val hint: String, initialText: String) {
var text by mutableStateOf(initialText)
val isHint: Boolean
get() = text == hint
}
Lớp phải có các đặc điểm sau:
text
là trạng thái khả biến thuộc loạiString
, tương tự như trạng thái ta có trongCraneEditableUserInput
. Quan trọng là bạn phải sử dụngmutableStateOf
để Compose theo dõi các thay đổi liên quan đến giá trị và biên soạn lại khi có thay đổi.text
là mộtvar
, do đó bạn có thể thay đổi trực tiếp từ bên ngoài lớp.- Lớp này lấy
initialText
làm phần phụ thuộc dùng để khởi chạytext
. - Logic để biết liệu
text
có phải là gợi ý hay không nằm trong thuộc tínhisHint
thực hiện việc kiểm tra theo yêu cầu.
Nếu logic ngày một phức tạp hơn, chúng ta chỉ cần thực hiện các thay đổi cho một lớp: EditableUserInputState
.
Ghi nhớ trình giữ trạng thái
Các trình giữ trạng thái luôn cần được ghi nhớ để có thể giữ chúng trong Composition và không phải lúc nào cũng tạo kênh mới. Nên tạo phương thức trong cùng một tệp để thực hiện việc này nhằm xóa bản mẫu và tránh mọi lỗi có thể xảy ra. Trong tệp base/EditableUserInput.kt
, thêm mã sau:
// base/EditableUserInput.kt file
@Composable
fun rememberEditableUserInputState(hint: String): EditableUserInputState =
remember(hint) {
EditableUserInputState(hint, hint)
}
Nếu chỉ có remember
trạng thái này, nó sẽ không tồn tại khi tạo lại hoạt động. Để trạng thái này tồn tại, chúng ta có thể dùng API rememberSaveable
thay vì API hoạt động tương tự như remember
. Tuy nhiên, giá trị được lưu trữ cũng vẫn tồn tại trong quá trình tạo lại hoạt động và xử lý. Trong cục bộ, nó sử dụng cơ chế trạng thái của phiên bản đã lưu.
rememberSaveable
thực hiện tất cả thao tác này mà không cần phải thực hiện thêm thao tác nào cho các đối tượng có thể được lưu trữ trong Bundle
. Đây không phải trường hợp cho lớp EditableUserInputState
mà chúng ta đã tạo trong dự án. Do đó, chúng ta cần cho rememberSaveable
biết cách lưu và khôi phục bản sao của lớp này sử dụng Saver
.
Tạo trình lưu tuỳ chỉnh
Saver
mô tả cách chuyển đổi một đối tượng thành Saveable
. Triển khai Saver
cần phải ghi đè hai hàm:
save
để chuyển đổi giá trị ban đầu thành một giá trị có thể lưu.restore
để chuyển đổi giá trị được khôi phục sang bản sao lớp ban đầu.
Đối với trường hợp của chúng ta, thay vì tạo phương thức triển khai tuỳ chỉnh Saver
cho lớp EditableUserInputState
, chúng ta có thể dùng một vài API Compose hiện có, chẳng hạn như listSaver
hoặc mapSaver
(mã lưu trữ các giá trị để lưu vào List
hoặc Map
) nhằm giảm số lượng mã cần viết.
Nên đặt các định nghĩa Saver
gần với lớp mà chúng tương tác. Vì cần truy cập tĩnh, nên hãy thêm Saver
cho EditableUserInputState
trong companion object
. Trong tệp base/EditableUserInput.kt
, thêm triển khai Saver
:
// base/EditableUserInput.kt file
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
class EditableUserInputState(private val hint: String, initialText: String) {
var text by mutableStateOf(initialText)
val isHint: Boolean
get() = text == hint
companion object {
val Saver: Saver<EditableUserInputState, *> = listSaver(
save = { listOf(it.hint, it.text) },
restore = {
EditableUserInputState(
hint = it[0],
initialText = it[1],
)
}
)
}
}
Trong trường hợp này, chúng ta sẽ sử dụng listSaver
làm chi tiết triển khai để lưu trữ và khôi phục bản sao EditableUserInputState
trong trình lưu.
Bây giờ, chúng ta có thể sử dụng trình lưu này trong rememberSaveable
(thay vì remember
) trong phương thức rememberEditableUserInputState
chúng ta đã tạo trước đây:
// base/EditableUserInput.kt file
import androidx.compose.runtime.saveable.rememberSaveable
@Composable
fun rememberEditableUserInputState(hint: String): EditableUserInputState =
rememberSaveable(hint, saver = EditableUserInputState.Saver) {
EditableUserInputState(hint, hint)
}
Bằng cách này, trạng thái đã ghi nhớ EditableUserInput
sẽ vẫn tồn tại khi tạo lại quy trình và hoạt động.
Sử dụng trình giữ trạng thái
Chúng ta sẽ sử dụng EditableUserInputState
thay vì text
và isHint
, nhưng chúng ta không muốn chỉ sử dụng nó làm trạng thái nội bộ trong CraneEditableUserInput
do thành phần kết hợp (composable) của người gọi không thể kiểm soát trạng thái. Thay vào đó, chúng ta muốn nâng EditableUserInputState
để các trình gọi có thể kiểm soát trạng thái của CraneEditableUserInput
. Nếu nâng trạng thái thì thành phần kết hợp (composable) có thể được sử dụng ở chế độ xem trước và kiểm thử dễ dàng hơn bởi bạn có thể sửa đổi trạng thái từ trình gọi.
Để thực hiện điều này, chúng ta cần thay đổi các thông số của hàm thành phần kết hợp (composable) và cung cấp giá trị mặc định nếu cần. Do chúng ta có thể sẽ muốn cho phép CraneEditableUserInput
có gợi ý trống, nên hãy thêm đối số mặc định:
@Composable
fun CraneEditableUserInput(
state: EditableUserInputState = rememberEditableUserInputState(""),
caption: String? = null,
@DrawableRes vectorImageId: Int? = null
) { /* ... */ }
Có thể bạn đã nhận thấy tham số onInputChanged
không còn tồn tại! Do trạng thái có thể được nâng lên nên nếu người gọi muốn biết liệu đầu vào có thay đổi hay không, họ có thể kiểm soát trạng thái và chuyển trạng thái đó vào hàm này.
Tiếp theo, chúng ta cần tinh chỉnh phần thân hàm để sử dụng trạng thái đã nâng thay vì trạng thái nội bộ đã dùng trước đó. Sau khi được cải tiến cấu trúc, hàm sẽ có dạng như sau:
@Composable
fun CraneEditableUserInput(
state: EditableUserInputState = rememberEditableUserInputState(""),
caption: String? = null,
@DrawableRes vectorImageId: Int? = null
) {
CraneBaseUserInput(
caption = caption,
tintIcon = { !state.isHint },
showCaption = { !state.isHint },
vectorImageId = vectorImageId
) {
BasicTextField(
value = state.text,
onValueChange = { state.text = it },
textStyle = if (state.isHint) {
captionTextStyle.copy(color = LocalContentColor.current)
} else {
MaterialTheme.typography.body1.copy(color = LocalContentColor.current)
},
cursorBrush = SolidColor(LocalContentColor.current)
)
}
}
Người gọi của trình giữ trạng thái
Do đã thay đổi API của CraneEditableUserInput
, nên chúng ta cần kiểm tra tất cả các vị trí API được gọi để đảm bảo chuyển đúng tham số.
Địa điểm duy nhất trong dự án mà chúng ta gọi API này là trong tệp home/SearchUserInput.kt
. Mở tệp đó và đến hàm thành phần kết hợp (composable) ToDestinationUserInput
, bạn sẽ thấy lỗi bản dựng ở đó. Do gợi ý hiện là một phần của trình giữ trạng thái và chúng ta muốn gợi ý tùy chỉnh cho bản sao này của CraneEditableUserInput
trong Composition, nên sẽ cần ghi nhớ trạng thái ở cấp độ ToDestinationUserInput
và chuyển trạng thái đó vào CraneEditableUserInput
:
// home/SearchUserInput.kt file
import androidx.compose.samples.crane.base.rememberEditableUserInputState
@Composable
fun ToDestinationUserInput(onToDestinationChanged: (String) -> Unit) {
val editableUserInputState = rememberEditableUserInputState(hint = "Choose Destination")
CraneEditableUserInput(
state = editableUserInputState,
caption = "To",
vectorImageId = R.drawable.ic_plane
)
}
snapshotFlow
Mã ở trên thiếu chức năng thông báo cho người gọi của ToDestinationUserInput
khi đầu vào thay đổi. Do cấu trúc của ứng dụng nên chúng ta không muốn nâng EditableUserInputState
lên cấp cao hơn bởi chúng ta muốn ghép thành phần kết hợp (composable) khác như FlySearchContent
với trạng thái này. Chúng ta có thể gọi hàm Lambda onToDestinationChanged
từ ToDestinationUserInput
như thế nào để vẫn có thể sử dụng lại thành phần kết hợp này?
Chúng ta có thể kích hoạt hiệu ứng phụ sử dụng LaunchedEffect
mỗi khi thay đổi dữ liệu đầu vào và gọi hàm lambda onToDestinationChanged
:
// home/SearchUserInput.kt file
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
@Composable
fun ToDestinationUserInput(onToDestinationChanged: (String) -> Unit) {
val editableUserInputState = rememberEditableUserInputState(hint = "Choose Destination")
CraneEditableUserInput(
state = editableUserInputState,
caption = "To",
vectorImageId = R.drawable.ic_plane
)
val currentOnDestinationChanged by rememberUpdatedState(onToDestinationChanged)
LaunchedEffect(editableUserInputState) {
snapshotFlow { editableUserInputState.text }
.filter { !editableUserInputState.isHint }
.collect {
currentOnDestinationChanged(editableUserInputState.text)
}
}
}
Chúng ta đã sử dụng LaunchedEffect
và rememberUpdatedState
trước đó, nhưng mã ở trên cũng sử dụng API mới! Chúng ta sử dụng snapshotFlow
API để chuyển đổi các đối tượng Compose State<T>
thành Flow (Luồng). Khi thông số trạng thái bên trong snapshotFlow
thay đổi, Flow (Luồng) sẽ phát ra giá trị mới trình thu thập. Trong trường hợp này, chúng ta chuyển đổi trạng thái thành luồng để sử dụng sức mạnh của toán tử luồng. Bằng cách đó, chúng ta filter
khi text
không phải là hint
và collect
các mục được tạo ra để thông báo cho cha mẹ điểm đến hiện tại đã thay đổi.
Không có thay đổi nào về hình ảnh trong bước này của lớp học lập trình, nhưng chúng ta đã cải thiện chất lượng của phần mã này. Nếu chạy ứng dụng ngay bây giờ, bạn sẽ thấy mọi thứ hoạt động như trước đây.
8. DisposableEffect
Khi nhấn vào một điểm đến, màn hình chi tiết sẽ mở ra và bạn có thể thấy vị trí của thành phố trên bản đồ. Mã đó nằm trong tệp details/DetailsActivity.kt
. Trong thành phần kết hợp (composable) CityMapView
, chúng ta gọi hàm rememberMapViewWithLifecycle
. Nếu mở hàm này vốn nằm trong tệp details/MapViewUtils.kt
, bạn sẽ thấy hàm không được kết nối với bất kỳ vòng đời nào! Chrome chỉ ghi nhớ MapView
và gọi onCreate
trên đó:
// details/MapViewUtils.kt file - code in the main branch
@Composable
fun rememberMapViewWithLifecycle(): MapView {
val context = LocalContext.current
// TODO Codelab: DisposableEffect step. Make MapView follow the lifecycle
return remember {
MapView(context).apply {
id = R.id.map
onCreate(Bundle())
}
}
}
Mặc dù ứng dụng chạy bình thường, nhưng đây là một vấn đề do MapView
không tuân theo đúng vòng đời. Do đó, bạn sẽ không biết khi nào ứng dụng chuyển sang chạy ngầm, khi nào nên tạm dừng View (Chế độ xem), v.v. Hãy cùng khắc phục lỗi này!
Do MapView
là View (Chế độ xem) chứ không phải thành phần kết hợp (composable) nên chúng ta muốn nó tuân theo vòng đời của Activity (Hoạt động) được sử dụng thay vì vòng đời của Composition. Điều đó có nghĩa là chúng ta cần tạo LifecycleEventObserver
để theo dõi các sự kiện trong vòng đời và gọi các phương thức phù hợp trên MapView
. Sau đó, chúng ta cần thêm trình quan sát này vào vòng đời của hoạt động hiện tại.
Hãy bắt đầu bằng cách tạo một hàm trả về LifecycleEventObserver
gọi phương thức tương ứng ở một MapView
trong một sự kiện nhất định:
// details/MapViewUtils.kt file
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
private fun getMapLifecycleObserver(mapView: MapView): LifecycleEventObserver =
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle())
Lifecycle.Event.ON_START -> mapView.onStart()
Lifecycle.Event.ON_RESUME -> mapView.onResume()
Lifecycle.Event.ON_PAUSE -> mapView.onPause()
Lifecycle.Event.ON_STOP -> mapView.onStop()
Lifecycle.Event.ON_DESTROY -> mapView.onDestroy()
else -> throw IllegalStateException()
}
}
Bây giờ, chúng ta cần thêm trình quan sát này vào vòng đời hiện tại để có thể sử dụng LifecycleOwner
hiện tại với thành phần (composition) LocalLifecycleOwner
cục bộ. Tuy nhiên, như vậy vẫn chưa đủ để thêm trình quan sát, chúng ta cũng cần phải xóa nó! Chúng ta cần hiệu ứng phụ để biết thời điểm hiệu ứng đó rời khỏi Composition để có thể thực hiện mã dọn dẹp. API hiệu ứng phụ mà chúng ta đang tìm kiếm là DisposableEffect
.
DisposableEffect
dành cho các hiệu ứng phụ cần được dọn dẹp sau khi các khóa thay đổi hoặc thành phần kết hợp (composable) rời khỏi Composition. Mã rememberMapViewWithLifecycle
cuối cùng sẽ hoạt động chính xác như vậy. Triển khai các dòng sau trong dự án của bạn:
// details/MapViewUtils.kt file
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalLifecycleOwner
@Composable
fun rememberMapViewWithLifecycle(): MapView {
val context = LocalContext.current
val mapView = remember {
MapView(context).apply {
id = R.id.map
}
}
val lifecycle = LocalLifecycleOwner.current.lifecycle
DisposableEffect(key1 = lifecycle, key2 = mapView) {
// Make MapView follow the current lifecycle
val lifecycleObserver = getMapLifecycleObserver(mapView)
lifecycle.addObserver(lifecycleObserver)
onDispose {
lifecycle.removeObserver(lifecycleObserver)
}
}
return mapView
}
Trình quan sát được thêm vào lifecycle
hiện tại và sẽ bị xoá khi vòng đời hiện tại thay đổi hoặc thành phần kết hợp này rời khỏi Composition (Cấu trúc). Với key
trong DisposableEffect
, nếu lifecycle
hoặc mapView
thay đổi, trình quan sát sẽ bị xoá rồi thêm lại vào lifecycle
bên phải.
Với những thay đổi chúng tôi vừa thực hiện, MapView
sẽ luôn tuân theo lifecycle
của LifecycleOwner
hiện tại và hành vi của nó sẽ như được sử dụng trong View (Chế độ xem).
Hãy thoải mái chạy ứng dụng này và mở màn hình chi tiết để đảm bảo MapView
vẫn hiển thị chính xác. Không có thay đổi nào về hình ảnh trong bước này.
9. produceState
Trong phần này, chúng ta sẽ cải thiện cách màn hình chi tiết khởi động. Thành phần kết hợp (composable) DetailsScreen
trong tệp details/DetailsActivity.kt
sẽ nhận cityDetails
đồng bộ từ ViewModel và gọi DetailsContent
nếu kết quả thành công.
Tuy nhiên, cityDetails
có thể sẽ tốn nhiều phí tải hơn trên luồng giao diện người dùng và có thể sử dụng coroutine để chuyển dữ liệu sang một luồng khác. Hãy cải thiện mã này để thêm màn hình tải và hiển thị DetailsContent
khi dữ liệu đã sẵn sàng.
Một cách để mô hình hoá trạng thái màn hình là sử dụng lớp sau đây, trong đó bao hàm mọi khả năng: dữ liệu cần hiển thị trên màn hình cũng như tín hiệu tải và lỗi. Thêm lớp DetailsUiState
vào tệp DetailsActivity.kt
:
// details/DetailsActivity.kt file
data class DetailsUiState(
val cityDetails: ExploreModel? = null,
val isLoading: Boolean = false,
val throwError: Boolean = false
)
Chúng ta có thể liên kết những nội dung màn hình cần hiển thị và UiState
trong lớp ViewModel sử dụng luồng dữ liệu, StateFlow
thuộc loại DetailsUiState
mà ViewModel cập nhật khi thông tin đã sẵn sàng và Compose thu thập bằng API collectAsState()
bạn đã biết.
Tuy nhiên, để thực hiện bài tập này, chúng ta sẽ triển khai một phương án thay thế. Nếu muốn di chuyển logic ánh xạ uiState
sang thế giới Compose, chúng ta có thể sử dụng API produceState
.
produceState
cho phép bạn chuyển đổi trạng thái không phải Compose thành Trạng thái Compose. Tệp này sẽ khởi chạy một coroutine trong phạm vi Composition, coroutine này có thể đẩy các giá trị vào State
được trả về bằng thuộc tính value
. Tương tự như LaunchedEffect
, produceState
cũng lấy các khóa để hủy và khởi động lại việc tính toán.
Trong trường hợp này, chúng ta có thể sử dụng produceState
để phát hành các bản cập nhật uiState
có giá trị ban đầu là DetailsUiState(isLoading = true)
như sau:
// details/DetailsActivity.kt file
import androidx.compose.runtime.produceState
@Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
// In a coroutine, this can call suspend functions or move
// the computation to different Dispatchers
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
}
// TODO: ...
}
Tiếp theo, tuỳ thuộc vào uiState
, chúng ta sẽ cho thấy dữ liệu, màn hình tải hoặc báo cáo lỗi. Dưới đây là mã hoàn chỉnh cho thành phần kết hợp (composable) DetailsScreen
:
// details/DetailsActivity.kt file
import androidx.compose.foundation.layout.Box
import androidx.compose.material.CircularProgressIndicator
@Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
}
when {
uiState.cityDetails != null -> {
DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
}
uiState.isLoading -> {
Box(modifier.fillMaxSize()) {
CircularProgressIndicator(
color = MaterialTheme.colors.onSurface,
modifier = Modifier.align(Alignment.Center)
)
}
}
else -> { onErrorLoading() }
}
}
Nếu chạy ứng dụng thì bạn sẽ thấy cách vòng quay đang tải xuất hiện trước khi cho thấy thông tin chi tiết về thành phố.
10. derivedStateOf
Cải tiến cuối cùng chúng ta sẽ thực hiện với ứng dụng Crane là hiển thị nút Scroll to top (Di chuyển lên đầu) bất cứ khi nào bạn di chuyển đến danh sách điểm đến của chuyến bay sau khi bạn đã chuyển thành phần đầu tiên của màn hình. Khi nhấn vào nút đó, bạn sẽ được chuyển đến thành phần đầu tiên trong danh sách.
Mở tệp base/ExploreSection.kt
chứa mã này. Thành phần kết hợp (composable) ExploreSection
tương ứng với những gì bạn thấy trong nền của giàn giáo.
Bạn nên lường trước giải pháp thực hiện hành vi hiển thị trên video. Tuy nhiên, có một API mới mà chúng ta chưa từng thấy, nó rất quan trọng trong trường hợp sử dụng này: API derivedStateOf
.
Sử dụng derivedStateOf
khi bạn muốn một State
Compose được hệ thống tạo từ State
khác. Sử dụng hàm này đảm bảo việc tính toán sẽ chỉ xảy ra khi nào một trong các trạng thái được sử dụng trong phép tính có sự thay đổi.
Để tính toán liệu người dùng có vượt qua mục đầu tiên hay không sử dụng listState
cũng đơn giản như việc kiểm tra xem listState.firstVisibleItemIndex > 0
có hay không. Tuy nhiên, firstVisibleItemIndex
được bao bọc trong API mutableStateOf
, điều này khiến nó trở thành Trạng thái Compose có thể quan sát. Tính toán của chúng ta cũng phải là một Trạng thái Compose vì chúng ta muốn soạn lại giao diện người dùng để hiển thị nút!
Ví dụ sau sẽ cho thấy cách triển khai đơn thuần và không hiệu quả. Đừng sao chép nó vào dự án; cách triển khai chính xác sẽ được sao chép vào dự án của bạn cùng phần còn lại của logic cho màn hình sau đó:
// DO NOT DO THIS - It's executed on every recomposition
val showButton = listState.firstVisibleItemIndex > 0
Một cách khác hiệu quả hơn là sử dụng API derivedStateOf
chỉ tính toán showButton
khi listState.firstVisibleItemIndex
thay đổi:
// Show the button if the first visible item is past
// the first item. We use a remembered derived state to
// minimize unnecessary compositions
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
Mã mới cho thành phần kết hợp (composable) ExploreSection
có thể đã quen thuộc với bạn. Hãy xem lại cách chúng tôi sử dụng rememberCoroutineScope
để gọi hàm tạm ngưng listState.scrollToItem
trong lệnh gọi lại onClick
của Button
. Chúng tôi sẽ sử dụng Box
để đặt Button
hiển thị có điều kiện lên đầu ExploreList
:
// base/ExploreSection.kt file
import androidx.compose.material.FloatingActionButton
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.foundation.layout.navigationBarsPadding
import kotlinx.coroutines.launch
@Composable
fun ExploreSection(
modifier: Modifier = Modifier,
title: String,
exploreList: List<ExploreModel>,
onItemClicked: OnExploreItemClicked
) {
Surface(modifier = modifier.fillMaxSize(), color = Color.White, shape = BottomSheetShape) {
Column(modifier = Modifier.padding(start = 24.dp, top = 20.dp, end = 24.dp)) {
Text(
text = title,
style = MaterialTheme.typography.caption.copy(color = crane_caption)
)
Spacer(Modifier.height(8.dp))
Box(Modifier.weight(1f)) {
val listState = rememberLazyListState()
ExploreList(exploreList, onItemClicked, listState = listState)
// Show the button if the first visible item is past
// the first item. We use a remembered derived state to
// minimize unnecessary compositions
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
if (showButton) {
val coroutineScope = rememberCoroutineScope()
FloatingActionButton(
backgroundColor = MaterialTheme.colors.primary,
modifier = Modifier
.align(Alignment.BottomEnd)
.navigationBarsPadding()
.padding(bottom = 8.dp),
onClick = {
coroutineScope.launch {
listState.scrollToItem(0)
}
}
) {
Text("Up!")
}
}
}
}
}
}
Nếu chạy ứng dụng, bạn sẽ thấy nút xuất hiện ở dưới cùng sau khi bạn di chuyển và chuyển thành phần đầu tiên của màn hình.
11. Xin chúc mừng!
Xin chúc mừng, bạn đã hoàn tất thành công lớp học lập trình này và tìm hiểu các khái niệm nâng cao về API trạng thái và hiệu ứng phụ trong ứng dụng Jetpack Compose!
Bạn đã tìm hiểu cách tạo trình giữ trạng thái, API hiệu ứng phụ như LaunchedEffect
, rememberUpdatedState
, DisposableEffect
, produceState
và derivedStateOf
cũng như cách sử dụng coroutine trong Jetpack Compose.
Tiếp theo là gì?
Hãy xem các lớp học lập trình khác trên Lộ trình Compose và các mã mẫu khác bao gồm Crane.
Tài liệu
Để biết thêm thông tin và hướng dẫn về những chủ đề này, vui lòng xem các tài liệu sau: