1. Giới thiệu
Compose và hệ thống Chế độ xem có thể hoạt động cùng nhau.
Trong lớp học lập trình này, bạn sẽ di chuyển các phần của màn hình thông tin chi tiết về cây trồng của Sunflower sang Compose. Chúng tôi đã tạo một bản sao của dự án để bạn có thể thử di chuyển ứng dụng thực tế sang Compose.
Khi kết thúc lớp học lập trình, bạn có thể tiếp tục di chuyển và chuyển đổi các màn hình còn lại của Sunflower nếu muốn.
Để được hỗ trợ thêm khi bạn tham gia lớp học lập trình này, hãy xem các mã sau:
Kiến thức bạn sẽ học được
Trong lớp học lập trình này, bạn sẽ tìm hiểu:
- Những lộ trình di chuyển khác bạn có thể sử dụng
- Cách di chuyển dần ứng dụng sang Compose
- Cách thêm Compose vào màn hình hiện có được tạo bằng Chế độ xem
- Cách dùng Thành phần hiển thị trong Compose
- Cách tạo giao diện trong Compose
- Cách kiểm thử màn hình hỗn hợp được viết trong cả Thành phần hiển thị và Compose
Điều kiện tiên quyết
- Kinh nghiệm về cú pháp Kotlin, bao gồm cả lambda.
- Hiểu rõ những kiến thức cơ bản về Compose
Bạn cần có
2. Chiến lược di chuyển
Ngay từ đầu, Jetpack Compose được thiết kế với trọng tâm là khả năng tương tác của thành phần hiển thị. Để di chuyển sang Compose, bạn nên di chuyển lần lượt tại vị trí Compose và thành phần hiển thị cùng tồn tại trong cơ sở mã cho đến khi ứng dụng của bạn hoàn toàn chuyển sang Compose.
Chiến lược di chuyển được đề xuất như sau:
- Tạo màn hình mới bằng Compose
- Khi bạn tạo tính năng, hãy xác định những thành phần có thể tái sử dụng rồi bắt đầu tạo thư viện gồm các thành phần giao diện người dùng phổ biến
- Thay thế các tính năng hiện có theo từng màn hình một
Tạo màn hình mới bằng Compose
Việc sử dụng Compose để tạo các tính năng mới bao gồm toàn bộ màn hình là cách tốt nhất để tăng tỷ lệ sử dụng Compose. Với chiến lược này, bạn có thể thêm nhiều tính năng và tận dụng lợi ích của Compose mà vẫn đáp ứng được nhu cầu kinh doanh của công ty.
Tính năng mới có thể bao gồm toàn bộ màn hình, trong trường hợp đó, toàn bộ màn hình sẽ nằm trong Compose. Nếu bạn đang sử dụng chế độ điều hướng dựa trên Mảnh, tức là bạn sẽ tạo một Mảnh mới kèm nội dung của Mảnh đó trong Compose.
Bạn cũng có thể thêm tính năng mới trên màn hình hiện có. Trong trường hợp này, Thành phần hiển thị và Compose sẽ cùng tồn tại trên một màn hình. Ví dụ: giả sử tính năng bạn đang thêm là một loại thành phần hiển thị mới trong RecyclerView. Trong trường hợp đó, loại chế độ xem mới sẽ nằm trong Compose mà vẫn giữ nguyên các mục khác.
Tạo thư viện gồm các thành phần giao diện người dùng phổ biến
Khi tạo tính năng bằng Compose, bạn sẽ nhanh chóng nhận ra rằng rồi bạn cũng sẽ tạo một thư viện thành phần. Bạn nên xác định các thành phần có thể sử dụng lại để thúc đẩy quá trình sử dụng lại trên ứng dụng, nhờ đó, các thành phần dùng chung sẽ có nguồn đáng tin cậy. Các tính năng mà bạn tạo có thể phụ thuộc vào thư viện này.
Thay thế tính năng hiện có bằng Compose
Ngoài việc xây dựng tính năng mới, bạn sẽ muốn di chuyển dần các tính năng hiện có trong ứng dụng của mình sang Compose. Bạn có thể quyết định cách xử lý này, nhưng sau đây là một số đề xuất phù hợp:
- Màn hình đơn giản – màn hình đơn giản trong ứng dụng có một số yếu tố giao diện người dùng và có tính linh động như màn hình chào mừng, màn hình xác nhận hoặc màn hình cài đặt. Đây là những đề xuất phù hợp để chuyển sang ứng dụng Compose vì bạn chỉ cần vài dòng mã.
- Màn hình Chế độ xem và Compose hỗn hợp – các màn hình đã chứa một ít mã Compose là một ứng cử viên phù hợp khác vì bạn có thể tiếp tục di chuyển các phần tử trong màn hình đó theo từng mảnh. Nếu có một màn hình chỉ có cây con trong Compose thì bạn có thể tiếp tục di chuyển các phần khác của cây cho đến khi toàn bộ giao diện người dùng nằm trong Compose. Đây được gọi là phương pháp di chuyển từ dưới lên.
Phương pháp trong Lớp học lập trình này
Trong lớp học lập trình này, bạn sẽ thực hiện việc di chuyển dần sang màn hình thông tin chi tiết về cây trồng của Sunflower có mục Compose và Chế độ xem hoạt động cùng nhau. Sau đó, bạn sẽ nắm rõ kiến thức đủ để tiếp tục việc di chuyển nếu muốn.
3. Thiết lập
Lấy mã
Lấy mã cho lớp học lập trình từ GitHub:
$ git clone https://github.com/android/codelab-android-compose
Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp ZIP:
Chạy ứng dụng mẫu
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 MigrationCodelab
trong Android Studio.
Trong lớp học lập trình này, bạn sẽ di chuyển màn hình thông tin chi tiết về cây trồng của Sunflower sang Compose. Có thể mở màn hình chi tiết về cây bằng cách nhấn vào một trong các cây có trong màn hình danh sách cây.
Thiết lập dự án
Dự án được xây dựng trong nhiều nhánh git.
- Nhánh
main
là điểm xuất phát của lớp học lập trình. end
chứa giải pháp cho lớp học lập trình này.
Bạn nên bắt đầu bằng 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 tiến độ phù hợp với bạ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 phải xoá mã được đề cập rõ ràng trong các nhận xét trên đoạn mã.
Để nhận nhánh end
bằng cách dùng git, hãy cd
vào thư mục của dự án MigrationCodelab
, rồi dùng lệnh:
$ git checkout end
Hoặc tải mã giải pháp từ đây:
Câu hỏi thường gặp
4. Compose trong Sunflower
Compose đã được thêm vào mã bạn tải xuống từ nhánh main
. Tuy nhiên, hãy xem những điều kiện để mã này có thể hoạt động.
Nếu mở tệp build.gradle
ở cấp ứng dụng, hãy xem cách tệp này nhập phần phụ thuộc Compose và cho phép Android Studio hoạt động với Compose bằng cách sử dụng cờ buildFeatures { compose true }
.
app/build.gradle
android {
//...
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
//...
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.3.2'
}
}
dependencies {
//...
// Compose
def composeBom = platform('androidx.compose:compose-bom:2024.09.02')
implementation(composeBom)
androidTestImplementation(composeBom)
implementation "androidx.compose.runtime:runtime"
implementation "androidx.compose.ui:ui"
implementation "androidx.compose.foundation:foundation"
implementation "androidx.compose.foundation:foundation-layout"
implementation "androidx.compose.material3:material3"
implementation "androidx.compose.runtime:runtime-livedata"
implementation "androidx.compose.ui:ui-tooling"
//...
}
Phiên bản của các phần phụ thuộc đó được xác định trong tệp build.gradle
cấp dự án.
5. Xin chào Compose!
Trong màn hình thông tin chi tiết về cây, chúng ta sẽ di chuyển nội dung mô tả về cây sang Compose, đồng thời giữ nguyên cấu trúc tổng thể của màn hình.
Compose cần có Hoạt động lưu trữ hoặc Mảnh để cho thấy giao diện người dùng. Trong Sunflower, vì tất cả màn hình đều sử dụng các mảnh nên bạn sẽ dùng ComposeView
: một Chế độ xem Android có thể lưu trữ nội dung trên giao diện người dùng của Compose bằng phương thức setContent
.
Xoá mã XML
Hãy bắt đầu di chuyển! Mở fragment_plant_detail.xml
và làm như sau:
- Chuyển sang Chế độ xem mã
- Xoá mã
ConstraintLayout
và 4TextView
được lồng bên trongNestedScrollView
(lớp học lập trình sẽ so sánh và tham chiếu đến mã XML khi di chuyển từng mục riêng lẻ, vì vậy, bạn sẽ thấy mã có nhận xét sẽ hữu ích) - Hãy thêm một
ComposeView
lưu trữ mã Compose thay vì vớicompose_view
để làm id chế độ xem
fragment_plant_detail.xml
<androidx.core.widget.NestedScrollView
android:id="@+id/plant_detail_scrollview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="@dimen/fab_bottom_padding"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<!-- Step 2) Comment out ConstraintLayout and its children –->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/margin_normal">
<TextView
android:id="@+id/plant_detail_name"
...
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- End Step 2) Comment out until here –->
<!-- Step 3) Add a ComposeView to host Compose code –->
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.core.widget.NestedScrollView>
Thêm mã Compose
Lúc này, bạn đã sẵn sàng để bắt đầu di chuyển màn hình thông tin chi tiết về cây trồng đến Compose!
Trong suốt lớp học lập trình, bạn phải thêm mã Compose vào tệp PlantDetailDescription.kt
trong thư mục plantdetail
. Mở tệp này và xem cách chúng tôi đã có một phần giữ chỗ cho văn bản "Hello Compose"
trong dự án.
PlantDetailDescription.kt
@Composable
fun PlantDetailDescription() {
Surface {
Text("Hello Compose")
}
}
Hãy cho thấy thông tin này trên màn hình bằng cách gọi thành phần kết hợp này qua ComposeView
mà chúng ta đã thêm ở bước trước. Mở PlantDetailFragment.kt
.
Vì màn hình đang sử dụng tính năng liên kết dữ liệu, nên bạn có thể truy cập trực tiếp vào composeView
và gọi setContent
để cho thấy mã Compose trên màn hình. Hãy gọi thành phần kết hợp PlantDetailDescription
bên trong MaterialTheme
vì Sunflower sử dụng Material Design.
PlantDetailFragment.kt
class PlantDetailFragment : Fragment() {
// ...
override fun onCreateView(...): View? {
val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
inflater, R.layout.fragment_plant_detail, container, false
).apply {
// ...
composeView.setContent {
// You're in Compose world!
MaterialTheme {
PlantDetailDescription()
}
}
}
// ...
}
}
Nếu chạy ứng dụng thì bạn có thể thấy "Hello Compose
" xuất hiện trên màn hình.
6. Tạo một Thành phần kết hợp từ XML
Hãy bắt đầu bằng cách di chuyển tên của cây. Chính xác hơn là TextView
với mã @+id/plant_detail_name
đã xoá trong fragment_plant_detail.xml
. Sau đây là mã XML:
<TextView
android:id="@+id/plant_detail_name"
...
android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:gravity="center_horizontal"
android:text="@{viewModel.plant.name}"
android:textAppearance="?attr/textAppearanceHeadline5"
... />
Hãy xem cách phần này có kiểu textAppearanceHeadline5
, có lề ngang là 8.dp
và nằm ở giữa màn hình theo chiều ngang. Tuy nhiên, tiêu đề cần hiện được quan sát qua LiveData
do PlantDetailViewModel
(bắt nguồn từ lớp kho lưu trữ) hiển thị.
Khi quan sát thấy LiveData
được đề cập sau đó, giả sử chúng ta có sẵn tên và tên được chuyển dưới dạng tham số vào thành phần kết hợp PlantName
mới được tạo trong tệp PlantDetailDescription.kt
. Thành phần kết hợp này sẽ được gọi từ thành phần kết hợp PlantDetailDescription
sau.
PlantDetailDescription.kt
@Composable
private fun PlantName(name: String) {
Text(
text = name,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(R.dimen.margin_small))
.wrapContentWidth(Alignment.CenterHorizontally)
)
}
@Preview
@Composable
private fun PlantNamePreview() {
MaterialTheme {
PlantName("Apple")
}
}
Bản xem trước:
Trong trường hợp:
- Kiểu của
Text
làMaterialTheme.typography.headlineSmall
, tương tự nhưtextAppearanceHeadline5
trong mã XML. - Đối tượng sửa đổi sẽ trang trí Văn bản sao cho giống với phiên bản XML:
- Đối tượng sửa đổi
fillMaxWidth
dùng để chiếm dung lượng tối đa hiện có. Việc này tương ứng với giá trịmatch_parent
của thuộc tínhlayout_width
trong mã XML. - Đối tượng sửa đổi
padding
dùng để áp dụng giá trị khoảng đệm ngangmargin_small
. Việc này tương ứng với việc khai báomarginStart
vàmarginEnd
trong XML. Giá trịmargin_small
cũng là tài nguyên phương diện hiện có được tìm nạp bằng hàm trợ giúpdimensionResource
. - Đối tượng sửa đổi
wrapContentWidth
dùng để căn giữa văn bản theo chiều ngang. Việc này cũng tương tự như việc áp dụnggravity
củacenter_horizontal
trong XML.
7. ViewModel và LiveData
Bây giờ, hãy kết nối tiêu đề với màn hình. Để làm việc đó, bạn cần tải dữ liệu bằng cách sử dụng PlantDetailViewModel
. Do đó, Compose tích hợp với ViewModel và LiveData.
ViewModel
Vì một thực thể của PlantDetailViewModel
được dùng trong Mảnh, nên chúng ta có thể truyền nó dưới dạng tham số cho PlantDetailDescription
.
Mở tệp PlantDetailDescription.kt
rồi thêm tham số PlantDetailViewModel
vào PlantDetailDescription
:
PlantDetailDescription.kt
@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
//...
}
Lúc này, hãy truyền thực thể của ViewModel khi gọi thành phần kết hợp này qua mảnh:
PlantDetailFragment.kt
class PlantDetailFragment : Fragment() {
...
override fun onCreateView(...): View? {
...
composeView.setContent {
MaterialTheme {
PlantDetailDescription(plantDetailViewModel)
}
}
}
}
LiveData
Bằng cách này, bạn đã có quyền truy cập vào trường LiveData<Plant>
của PlantDetailViewModel
để lấy tên của cây.
Để quan sát LiveData qua thành phần kết hợp, hãy dùng hàm LiveData.observeAsState()
.
Vì giá trị do LiveData đưa ra có thể là null
, nên bạn cần gói hoạt động sử dụng LiveData trong quy trình kiểm tra null
. Do đó, để có thể tái sử dụng, tốt nhất hãy chia nhỏ mức tiêu thụ LiveData và nghe trong nhiều thành phần kết hợp. Hãy tạo một thành phần kết hợp mới có tên là PlantDetailContent
để cho thấy thông tin về Plant
.
Với những bản cập nhật này, tệp PlantDetailDescription.kt
giờ đây sẽ có dạng như sau:
PlantDetailDescription.kt
@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
// Observes values coming from the VM's LiveData<Plant> field
val plant by plantDetailViewModel.plant.observeAsState()
// If plant is not null, display the content
plant?.let {
PlantDetailContent(it)
}
}
@Composable
fun PlantDetailContent(plant: Plant) {
PlantName(plant.name)
}
@Preview
@Composable
private fun PlantDetailContentPreview() {
val plant = Plant("id", "Apple", "description", 3, 30, "")
MaterialTheme {
PlantDetailContent(plant)
}
}
PlantNamePreview
sẽ phản ánh thay đổi mà không cần cập nhật trực tiếp vì PlantDetailContent
chỉ gọi PlantName
:
Lúc này, bạn đã kết nối với ViewModel để cho thấy tên cây trong Compose. Trong một số phần tiếp theo, bạn sẽ xây dựng các thành phần kết hợp còn lại rồi kết nối chúng với ViewModel theo cách tương tự.
8. Di chuyển mã XML khác
Giờ đây, bạn đã có thể dễ dàng hoàn thành những việc còn thiếu trong giao diện người dùng: thông tin tưới cây và nội dung mô tả cây cối. Khi làm theo cách tiếp cận tương tự như trước đây, bạn có thể di chuyển phần còn lại của màn hình.
Mã XML thông tin tưới cây bạn đã xoá trước đó khỏi fragment_plant_detail.xml
bao gồm hai chế độ TextView có mã plant_watering_header
và plant_watering
.
<TextView
android:id="@+id/plant_watering_header"
...
android:layout_marginStart="@dimen/margin_small"
android:layout_marginTop="@dimen/margin_normal"
android:layout_marginEnd="@dimen/margin_small"
android:gravity="center_horizontal"
android:text="@string/watering_needs_prefix"
android:textColor="?attr/colorAccent"
android:textStyle="bold"
... />
<TextView
android:id="@+id/plant_watering"
...
android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:gravity="center_horizontal"
app:wateringText="@{viewModel.plant.wateringInterval}"
.../>
Tương tự như cách bạn đã làm trước đó, hãy tạo một thành phần kết hợp mới tên là PlantWatering
rồi thêm thành phần kết hợp Text
để cho thấy thông tin tưới cây trên màn hình:
PlantDetailDescription.kt
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun PlantWatering(wateringInterval: Int) {
Column(Modifier.fillMaxWidth()) {
// Same modifier used by both Texts
val centerWithPaddingModifier = Modifier
.padding(horizontal = dimensionResource(R.dimen.margin_small))
.align(Alignment.CenterHorizontally)
val normalPadding = dimensionResource(R.dimen.margin_normal)
Text(
text = stringResource(R.string.watering_needs_prefix),
color = MaterialTheme.colorScheme.primaryContainer,
fontWeight = FontWeight.Bold,
modifier = centerWithPaddingModifier.padding(top = normalPadding)
)
val wateringIntervalText = pluralStringResource(
R.plurals.watering_needs_suffix, wateringInterval, wateringInterval
)
Text(
text = wateringIntervalText,
modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
)
}
}
@Preview
@Composable
private fun PlantWateringPreview() {
MaterialTheme {
PlantWatering(7)
}
}
Bản xem trước:
Một số điều cần lưu ý:
- Vì thành phần kết hợp
Text
chia sẻ khoảng đệm ngang và trang trí căn chỉnh nên bạn có thể sử dụng lại Đối tượng sửa đổi bằng cách chỉ định đối tượng này cho một biến cục bộ (tức làcenterWithPaddingModifier
). Vì đối tượng sửa đổi là đối tượng Kotlin thông thường, nên bạn có thể thực hiện việc này. MaterialTheme
của Compose không có kết quả khớp chính xác vớicolorAccent
được sử dụng trongplant_watering_header
. Lúc này, hãy dùngMaterialTheme.colorScheme.primaryContainer
mà bạn sẽ cải thiện trong phần giao diện khả năng tương tác.- Trong Compose 1. 2.1, để dùng
pluralStringResource
, bạn phải chọn dùngExperimentalComposeUiApi
. Trong một phiên bản tương lai của Compose, tính năng này có thể không cần thiết nữa.
Hãy kết nối tất cả các phần với nhau và cũng gọi PlantWatering
từ PlantDetailContent
. Mã ConstraintLayout XML chúng ta xoá ở phần đầu có lề 16.dp
chúng ta cần đưa vào mã Compose.
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/margin_normal">
Trong PlantDetailContent
, hãy tạo Column
để cho thấy cả tên và thông tin tưới nước, đồng thời dùng làm khoảng đệm. Ngoài ra, để màu nền và màu văn bản được dùng đều phù hợp, hãy thêm Surface
để xử lý vấn đề này.
PlantDetailDescription.kt
@Composable
fun PlantDetailContent(plant: Plant) {
Surface {
Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
PlantName(plant.name)
PlantWatering(plant.wateringInterval)
}
}
}
Nếu làm mới bản xem trước, bạn sẽ thấy:
9. Chế độ xem trong mã Compose
Bây giờ, hãy di chuyển phần mô tả cây. Mã trong fragment_plant_detail.xml
có một TextView
với app:renderHtml="@{viewModel.plant.description}"
để cho XML biết cần cho thấy văn bản nào trên màn hình. renderHtml
là phương thức điều hợp liên kết (binding adapter) có trong tệp PlantDetailBindingAdapters.kt
. Cách triển khai sử dụng HtmlCompat.fromHtml
để đặt văn bản trên TextView
!
Tuy nhiên, Compose hiện không hỗ trợ các lớp Spanned
cũng như không cho thấy văn bản có định dạng HTML. Do đó, chúng ta cần dùng TextView
từ hệ thống Chế độ xem trong mã Compose để bỏ qua giới hạn này.
Vì Compose chưa thể kết xuất mã HTML nên bạn sẽ tạo TextView
theo phương thức lập trình để thực hiện chính xác việc đó bằng API AndroidView
.
AndroidView
cho phép bạn tạo View
trong hàm lambda factory
của nó. Mã này cũng cung cấp lambda update
được gọi khi Khung hiển thị đã được tăng cường và trong các quy trình kết hợp lại tiếp theo.
Hãy thực hiện việc này bằng cách tạo một thành phần kết hợp PlantDescription
mới. Thành phần kết hợp này gọi AndroidView
để tạo TextView
trong hàm lambda factory
. Trong hàm lambda factory
, hãy khởi chạy TextView
để cho thấy văn bản ở định dạng HTML, sau đó đặt movementMethod
thành một phiên bản của LinkMovementMethod
. Cuối cùng, trong hàm lambda update
, hãy đặt văn bản của TextView
thành htmlDescription
.
PlantDetailDescription.kt
@Composable
private fun PlantDescription(description: String) {
// Remembers the HTML formatted description. Re-executes on a new description
val htmlDescription = remember(description) {
HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_COMPACT)
}
// Displays the TextView on the screen and updates with the HTML description when inflated
// Updates to htmlDescription will make AndroidView recompose and update the text
AndroidView(
factory = { context ->
TextView(context).apply {
movementMethod = LinkMovementMethod.getInstance()
}
},
update = {
it.text = htmlDescription
}
)
}
@Preview
@Composable
private fun PlantDescriptionPreview() {
MaterialTheme {
PlantDescription("HTML<br><br>description")
}
}
Xem trước:
Lưu ý htmlDescription
ghi nhớ mô tả HTML cho description
nhất định được chuyển dưới dạng thông số. Nếu tham số description
thay đổi thì mã htmlDescription
bên trong remember
sẽ thực thi lại.
Do đó, lệnh gọi lại cập nhật AndroidView
sẽ khởi tạo lại nếu htmlDescription
thay đổi. Mọi trạng thái được đọc bên trong lambda update
đều dẫn đến quá trình kết hợp lại.
Hãy thêm PlantDescription
vào thành phần kết hợp PlantDetailContent
rồi thay đổi mã xem trước để cho thấy nội dung mô tả HTML:
PlantDetailDescription.kt
@Composable
fun PlantDetailContent(plant: Plant) {
Surface {
Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
PlantName(plant.name)
PlantWatering(plant.wateringInterval)
PlantDescription(plant.description)
}
}
}
@Preview
@Composable
private fun PlantDetailContentPreview() {
val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
MaterialTheme {
PlantDetailContent(plant)
}
}
Bản xem trước:
Lúc này, bạn đã di chuyển tất cả nội dung bên trong ConstraintLayout
gốc sang Compose. Bạn có thể chạy ứng dụng để kiểm tra xem ứng dụng có hoạt động như mong đợi không.
10. Phương thức ViewCompositionStrategy
Compose sẽ huỷ bỏ Cấu trúc (Composition) bất cứ khi nào ComposeView
bị tách khỏi cửa sổ. Điều này là ngoài mong muốn khi ComposeView
được dùng trong mảnh vì 2 lý do:
- Cấu trúc phải tuân thủ vòng đời chế độ xem của mảnh đối với loại
View
giao diện người dùng của Compose để lưu trạng thái. - Khi quá trình chuyển đổi xảy ra,
ComposeView
cơ bản sẽ ở trạng thái tách rời. Tuy nhiên, các thành phần trên giao diện người dùng Compose vẫn sẽ xuất hiện trong quá trình chuyển đổi này.
Để sửa đổi hành vi này, hãy gọi setViewCompositionStrategy
cùng ViewCompositionStrategy
thích hợp để tuân thủ vòng đời chế độ xem của mảnh. Cụ thể là bạn nên sử dụng chiến lược DisposeOnViewTreeLifecycleDestroyed
để loại bỏ Cấu trúc khi LifecycleOwner
của mảnh bị huỷ bỏ.
Vì PlantDetailFragment
có chuyển đổi vào (enter) và thoát (exit) (xem nav_garden.xml
để biết thêm thông tin) nên chúng ta sẽ dùng loại View
trong Compose vào lúc khác, điều cần làm lúc này là đảm bảo ComposeView
sử dụng chiến lược DisposeOnViewTreeLifecycleDestroyed
. Tuy nhiên, phương pháp hay nhất là luôn đặt chiến lược này khi sử dụng ComposeView
trong mảnh.
PlantDetailFragment.kt
import androidx.compose.ui.platform.ViewCompositionStrategy
...
class PlantDetailFragment : Fragment() {
...
override fun onCreateView(...): View? {
val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
inflater, R.layout.fragment_plant_detail, container, false
).apply {
...
composeView.apply {
// Dispose the Composition when the view's LifecycleOwner
// is destroyed
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
MaterialTheme {
PlantDetailDescription(plantDetailViewModel)
}
}
}
}
...
}
}
11. Tuỳ chỉnh giao diện Material
Chúng ta đã di chuyển nội dung văn bản của thông tin chi tiết về cây sang Compose. Tuy nhiên, bạn có thể nhận thấy Compose không sử dụng đúng màu giao diện. Tên cây đang là màu tím, trong khi đáng ra phải là màu xanh lục.
Để sử dụng đúng màu giao diện, bạn cần tuỳ chỉnh MaterialTheme
bằng cách xác định giao diện riêng và chọn màu sắc.
Tuỳ chỉnh MaterialTheme
Để tạo giao diện riêng, hãy mở tệp Theme.kt
trong gói theme
. Theme.kt
xác định một thành phần kết hợp có tên là SunflowerTheme
chấp nhận lambda nội dung và truyền nội dung đó xuống MaterialTheme
.
Thành phần này chưa thực hiện bất kỳ chức năng thú vị nào. Bạn sẽ tuỳ chỉnh thành phần này ở bước tiếp theo.
Theme.kt
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@Composable
fun SunflowerTheme(
content: @Composable () -> Unit
) {
MaterialTheme(content = content)
}
MaterialTheme
hỗ trợ bạn tuỳ chỉnh màu sắc, kiểu chữ và hình dạng. Lúc này, hãy tiếp tục và tuỳ chỉnh màu sắc bằng cách chọn màu sắc tương tự trong giao diện của Sunflower View. SunflowerTheme
cũng có thể chấp nhận tham số boolean có tên darkTheme
. Tham số này sẽ mặc định là true
nếu hệ thống ở chế độ tối, nếu không thì là false
. Bằng cách sử dụng tham số này, chúng ta có thể truyền các giá trị màu phù hợp đến MaterialTheme
để khớp với giao diện hệ thống hiện được áp dụng.
Theme.kt
@Composable
fun SunflowerTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val lightColors = lightColorScheme(
primary = colorResource(id = R.color.sunflower_green_500),
primaryContainer = colorResource(id = R.color.sunflower_green_700),
secondary = colorResource(id = R.color.sunflower_yellow_500),
background = colorResource(id = R.color.sunflower_green_500),
onPrimary = colorResource(id = R.color.sunflower_black),
onSecondary = colorResource(id = R.color.sunflower_black),
)
val darkColors = darkColorScheme(
primary = colorResource(id = R.color.sunflower_green_100),
primaryContainer = colorResource(id = R.color.sunflower_green_200),
secondary = colorResource(id = R.color.sunflower_yellow_300),
onPrimary = colorResource(id = R.color.sunflower_black),
onSecondary = colorResource(id = R.color.sunflower_black),
onBackground = colorResource(id = R.color.sunflower_black),
surface = colorResource(id = R.color.sunflower_green_100_8pc_over_surface),
onSurface = colorResource(id = R.color.sunflower_white),
)
val colors = if (darkTheme) darkColors else lightColors
MaterialTheme(
colorScheme = colors,
content = content
)
}
Để sử dụng loại này, hãy thay thế MaterialTheme
được sử dụng cho SunflowerTheme
. Ví dụ: trong PlantDetailFragment
:
PlantDetailFragment.kt
class PlantDetailFragment : Fragment() {
...
composeView.apply {
...
setContent {
SunflowerTheme {
PlantDetailDescription(plantDetailViewModel)
}
}
}
}
Và tất cả thành phần kết hợp xem trước trong tệp PlantDetailDescription.kt
:
PlantDetailDescription.kt
@Preview
@Composable
private fun PlantDetailContentPreview() {
val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
SunflowerTheme {
PlantDetailContent(plant)
}
}
@Preview
@Composable
private fun PlantNamePreview() {
SunflowerTheme {
PlantName("Apple")
}
}
@Preview
@Composable
private fun PlantWateringPreview() {
SunflowerTheme {
PlantWatering(7)
}
}
@Preview
@Composable
private fun PlantDescriptionPreview() {
SunflowerTheme {
PlantDescription("HTML<br><br>description")
}
}
Như bạn có thể thấy trong bản xem trước, màu sắc hiện đã khớp với màu của giao diện Sunflower.
Bạn cũng có thể xem trước giao diện người dùng trong giao diện tối bằng cách tạo một hàm mới và chuyển Configuration.UI_MODE_NIGHT_YES
đến uiMode
của bản xem trước:
import android.content.res.Configuration
...
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PlantDetailContentDarkPreview() {
val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
SunflowerTheme {
PlantDetailContent(plant)
}
}
Bản xem trước:
Nếu bạn chạy ứng dụng thì ứng dụng sẽ hoạt động y như trước khi di chuyển, đối với cả giao diện sáng lẫn tối:
12. Thử nghiệm
Sau khi di chuyển các phần của màn hình thông tin chi tiết về cây sang Compose, bạn phải kiểm thử để đảm bảo không có bất cứ vấn đề gì.
Trong Sunflower, PlantDetailFragmentTest
nằm trong thư mục androidTest
sẽ kiểm tra một số chức năng của ứng dụng. Hãy mở tệp và xem mã hiện tại:
testPlantName
kiểm tra tên của cây trên màn hìnhtestShareTextIntent
kiểm tra để đảm bảo ý định phù hợp được kích hoạt sau khi nhấn vào nút chia sẻ
Khi một hoạt động hoặc mảnh sử dụng Compose, thay vì dùng ActivityScenarioRule
, bạn phải dùng createAndroidComposeRule
tích hợp ActivityScenarioRule
với ComposeTestRule
để có thể kiểm thử mã Compose.
Trong PlantDetailFragmentTest
, hãy thay thế cách sử dụng ActivityScenarioRule
bằng createAndroidComposeRule
. Khi cần có quy tắc hoạt động để định cấu hình kiểm thử, hãy dùng thuộc tính activityRule
của createAndroidComposeRule
theo cách sau:
@RunWith(AndroidJUnit4::class)
class PlantDetailFragmentTest {
@Rule
@JvmField
val composeTestRule = createAndroidComposeRule<GardenActivity>()
...
@Before
fun jumpToPlantDetailFragment() {
populateDatabase()
composeTestRule.activityRule.scenario.onActivity { gardenActivity ->
activity = gardenActivity
val bundle = Bundle().apply { putString("plantId", "malus-pumila") }
findNavController(activity, R.id.nav_host).navigate(R.id.plant_detail_fragment, bundle)
}
}
...
}
Nếu bạn chạy thử nghiệm, testPlantName
sẽ không thành công! testPlantName
kiểm tra để tìm một TextView hiển thị trên màn hình. Tuy nhiên, bạn đã di chuyển phần đó trong giao diện người dùng sang Compose. Do đó, bạn cần phải sử dụng tính năng Xác nhận Compose:
@Test
fun testPlantName() {
composeTestRule.onNodeWithText("Apple").assertIsDisplayed()
}
Nếu chạy thử, bạn sẽ thấy tất cả các thử nghiệm này đạt.
13. 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!
Nhánh compose
của dự án github Sunflower ban đầu sẽ di chuyển toàn bộ màn hình thông tin chi tiết về cây sang Compose. Ngoài những việc đã hoàn thành trong lớp học lập trình này, bạn còn cần mô phỏng hành vi của CollapsingThanhLayout. Việc này bao gồm:
- Tải hình ảnh bằng Compose
- Ảnh động
- Xử lý phương diện tốt hơn
- Và nhiều kiến thức khác!
Nội dung tiếp theo
Hãy tham khảo các lớp học lập trình khác trên Lộ trình học Compose.