Mặc dù việc di chuyển từ Khung hiển thị sang Compose chỉ liên quan đến giao diện người dùng, nhưng bạn cần xem xét nhiều thứ để có thể di chuyển dần và an toàn. Trang này trình bày một số điểm cần cân nhắc trong khi di chuyển ứng dụng dựa trên Khung hiển thị sang Compose.
Di chuyển giao diện của ứng dụng
Bạn nên sử dụng hệ thống thiết kế Material Design để thiết kế giao diện cho các ứng dụng Android.
Có 3 phiên bản Material dành cho các ứng dụng dựa trên Khung hiển thị:
- Material Design 1 dùng thư viện AppCompat (cụ thể là
Theme.AppCompat.*
) - Material Design 2 dùng thư viện MDC-Android (cụ thể là
Theme.MaterialComponents.*
) - Material Design 3 dùng thư viện MDC-Android (cụ thể là
Theme.Material3.*
)
Có 2 phiên bản Material dành cho các ứng dụng Compose:
- Material Design 2 dùng thư viện Compose Material (cụ thể là
androidx.compose.material.MaterialTheme
) - Material Design 3 dùng thư viện Compose Material 3 (cụ thể là
androidx.compose.material3.MaterialTheme
)
Nếu có thể, bạn nên sử dụng phiên bản mới nhất (Material 3) trong trường hợp hệ thống thiết kế của ứng dụng cho phép. Bạn có thể tham khảo hướng dẫn di chuyển cho cả Khung hiển thị và Compose theo đường liên kết dưới đây:
- Material 1 sang Material 2 trong Khung hiển thị
- Material 2 sang Material 3 trong Khung hiển thị
- Material 2 sang Material 3 trong Compose
Khi tạo màn hình mới trong ứng dụng Compose, bất kể bạn đang sử dụng phiên bản Material Design nào, hãy đảm bảo rằng bạn áp dụng MaterialTheme
trước mọi thành phần kết hợp tạo ra giao diện người dùng từ thư viện Compose Material. Các thành phần Material (Button
, Text
, v.v.) phụ thuộc vào việc có sẵn MaterialTheme
hay không. Nếu không có, thì hành vi của các thành phần đó sẽ không được xác định.
Tất cả mẫu Jetpack Compose đều sử dụng giao diện Compose tuỳ chỉnh dựa trên MaterialTheme
.
Hãy xem các bài viết Hệ thống thiết kế trong Compose và Di chuyển giao diện XML sang Compose để tìm hiểu thêm.
Di chuyển
Nếu bạn sử dụng thành phần Điều hướng trong ứng dụng, hãy xem phần Điều hướng bằng Compose – Khả năng tương tác và Di chuyển Điều hướng Jetpack sang Điều hướng Compose để biết thêm thông tin.
Kiểm thử giao diện người dùng hỗn hợp cho Compose/Khung hiển thị
Sau khi di chuyển các phần của ứng dụng sang Compose, việc kiểm thử là rất quan trọng để đảm bảo không có bất cứ vấn đề gì.
Khi một hoạt động hoặc mảnh sử dụng Compose, bạn cần dùng createAndroidComposeRule
thay vì sử dụng ActivityScenarioRule
. createAndroidComposeRule
tích hợp ActivityScenarioRule
với ComposeTestRule
cho phép bạn kiểm thử cùng lúc mã Compose và Khung hiển thị.
class MyActivityTest { @Rule @JvmField val composeTestRule = createAndroidComposeRule<MyActivity>() @Test fun testGreeting() { val greeting = InstrumentationRegistry.getInstrumentation() .targetContext.resources.getString(R.string.greeting) composeTestRule.onNodeWithText(greeting).assertIsDisplayed() } }
Hãy xem bài viết Kiểm thử bố cục Compose để tìm hiểu thêm về việc kiểm thử. Để biết khả năng tương tác với các khung kiểm thử giao diện người dùng, hãy xem bài viết khả năng tương tác với Espresso và khả năng tương tác với UiAutomator.
Tích hợp Compose vào cấu trúc ứng dụng hiện có
Mô hình cấu trúc Luồng dữ liệu một chiều (UDF) hoạt động liền mạch với Compose. Nếu ứng dụng dùng các loại mô hình cấu trúc khác, chẳng hạn như Mô hình – Khung hiển thị – Thành phần trình bày (MVP), bạn nên di chuyển phần giao diện người dùng đó sang UDF trước hoặc trong khi sử dụng Compose.
Sử dụng ViewModel
trong Compose
Nếu sử dụng thư viện Thành phần cấu trúcViewModel
, bạn có thể truy cập vào ViewModel
từ bất kỳ thành phần kết hợp nào bằng cách gọi hàm viewModel()
như giải thích trong bài viết Compose và các thư viện khác.
Khi dùng Compose, hãy cẩn thận về việc sử dụng cùng một loại ViewModel
trong nhiều thành phần kết hợp dưới dạng phần tử ViewModel
tuân theo phạm vi vòng đời của Khung hiển thị. Phạm vi này sẽ là hoạt động lưu trữ, mảnh hoặc biểu đồ điều hướng nếu sử dụng thư viện Điều hướng.
Ví dụ: nếu các thành phần kết hợp được lưu trữ trong một hoạt động, thì viewModel()
luôn trả về cùng một thực thể, thực thể này chỉ bị xoá khi hoạt động đó kết thúc.
Ở ví dụ sau, cùng một người dùng ("user1") sẽ được chào hai lần vì cùng một thực thể GreetingViewModel
được sử dụng lại trong mọi thành phần kết hợp dưới hoạt động lưu trữ. Thực thể ViewModel
đầu tiên đã tạo được sử dụng lại trong các thành phần kết hợp khác.
class GreetingActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { Column { GreetingScreen("user1") GreetingScreen("user2") } } } } } @Composable fun GreetingScreen( userId: String, viewModel: GreetingViewModel = viewModel( factory = GreetingViewModelFactory(userId) ) ) { val messageUser by viewModel.message.observeAsState("") Text(messageUser) } class GreetingViewModel(private val userId: String) : ViewModel() { private val _message = MutableLiveData("Hi $userId") val message: LiveData<String> = _message } class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { return GreetingViewModel(userId) as T } }
Biểu đồ điều hướng cũng xác định phạm vi các phần tử ViewModel
. Vì thế, những thành phần kết hợp là đích đến trong biểu đồ điều hướng sẽ có một thực thể ViewModel
khác.
Trong trường hợp này, ViewModel
nằm trong phạm vi vòng đời của đích đến và sẽ bị xoá khi đích đến bị xoá khỏi ngăn xếp lùi. Trong ví dụ sau đây, khi người dùng chuyển đến màn hình Hồ sơ, thao tác này sẽ tạo một thực thể mới của GreetingViewModel
.
@Composable fun MyApp() { NavHost(rememberNavController(), startDestination = "profile/{userId}") { /* ... */ composable("profile/{userId}") { backStackEntry -> GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "") } } }
Nguồn trạng thái đáng tin
Khi bạn sử dụng Compose trong một phần của giao diện người dùng, có thể Compose và mã hệ thống Khung hiển thị sẽ phải chia sẻ dữ liệu. Khi có thể, bạn nên đóng gói trạng thái được chia sẻ đó trong một lớp khác tuân theo các phương pháp hay nhất của UDF mà cả hai nền tảng sử dụng, chẳng hạn như trong ViewModel
hiển thị luồng dữ liệu được chia sẻ để phát các nội dung cập nhật dữ liệu.
Tuy nhiên, không phải lúc nào điều đó cũng có thể xảy ra nếu dữ liệu được chia sẻ có thể thay đổi hoặc liên kết chặt chẽ với một phần tử trên giao diện người dùng. Trong trường hợp đó, một hệ thống phải là nguồn đáng tin và hệ thống đó cần chia sẻ mọi nội dung cập nhật dữ liệu cho hệ thống kia. Theo quy tắc chung, nguồn đáng tin phải thuộc sở hữu của thành phần gần hơn với gốc của hệ phân cấp giao diện người dùng.
Compose là nguồn đáng tin
Sử dụng thành phần kết hợp SideEffect
để xuất bản trạng thái Compose thành các mã không phải Compose. Trong trường hợp này, nguồn đáng tin sẽ được giữ lại trong một thành phần kết hợp có nhiệm vụ gửi các lượt cập nhật trạng thái.
Ví dụ: thư viện phân tích của bạn có thể cho phép bạn phân đoạn toàn bộ số người dùng bằng cách đính kèm siêu dữ liệu tuỳ chỉnh (thuộc tính người dùng trong ví dụ này) vào mọi sự kiện phân tích tiếp theo. Để truyền đạt thông tin loại người dùng của người dùng hiện tại cho thư viện phân tích, hãy sử dụng SideEffect
để cập nhật giá trị.
@Composable fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { FirebaseAnalytics() } // On every successful composition, update FirebaseAnalytics with // the userType from the current User, ensuring that future analytics // events have this metadata attached SideEffect { analytics.setUserProperty("userType", user.userType) } return analytics }
Để biết thêm thông tin, hãy xem trang Hiệu ứng phụ trong Compose.
Hệ thống khung hiển thị là nguồn đáng tin
Nếu hệ thống chế độ xem sở hữu trạng thái và chia sẻ trạng thái đó với Compose, bạn nên bao gồm trạng thái
đó trong các đối tượng mutableStateOf
để trạng thái an toàn theo luồng cho
Compose. Nếu bạn sử dụng cách tiếp cận này, các hàm có khả năng kết hợp được đơn giản hoá vì các hàm đó không còn nguồn đáng tin nữa. Tuy nhiên, hệ thống Khung hiển thị cần cập nhật trạng thái có thể thay đổi và các Khung hiển thị sử dụng trạng thái đó.
Trong ví dụ sau, một CustomViewGroup
chứa TextView
và ComposeView
có thành phần kết hợp TextField
bên trong. TextView
cần hiển thị nội dung mà người dùng nhập vào trong TextField
.
class CustomViewGroup @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : LinearLayout(context, attrs, defStyle) { // Source of truth in the View system as mutableStateOf // to make it thread-safe for Compose private var text by mutableStateOf("") private val textView: TextView init { orientation = VERTICAL textView = TextView(context) val composeView = ComposeView(context).apply { setContent { MaterialTheme { TextField(value = text, onValueChange = { updateState(it) }) } } } addView(textView) addView(composeView) } // Update both the source of truth and the TextView private fun updateState(newValue: String) { text = newValue textView.text = newValue } }
Di chuyển giao diện người dùng dùng chung
Nếu đang chuyển dần sang ứng dụng Compose, bạn có thể cần sử dụng các phần tử dùng chung trên giao diện người dùng trong cả Compose và hệ thống Khung hiển thị. Ví dụ: nếu ứng dụng của bạn có một thành phần CallToActionButton
tuỳ chỉnh, thì bạn có thể cần sử dụng thành phần đó trong cả màn hình Compose và màn hình dựa trên Khung hiển thị.
Trong Compose, các phần tử dùng chung trên giao diện người dùng sẽ trở thành các thành phần kết hợp có thể dùng lại trên ứng dụng, bất kể phần tử đó được tạo kiểu bằng XML hay khung hiển thị tuỳ chỉnh. Ví dụ: bạn sẽ tạo một thành phần kết hợp CallToActionButton
cho thành phần Button
kêu gọi hành động tuỳ chỉnh.
Để sử dụng thành phần kết hợp này trong màn hình dựa trên Khung hiển thị, hãy tạo một trình bao bọc khung hiển thị tuỳ chỉnh mở rộng từ AbstractComposeView
. Trong thành phần kết hợp Content
bị ghi đè, hãy đặt thành phần kết hợp mà bạn tạo trong giao diện Compose như minh hoạ ở ví dụ dưới đây:
@Composable fun CallToActionButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, ) { Button( colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.secondary ), onClick = onClick, modifier = modifier, ) { Text(text) } } class CallToActionViewButton @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : AbstractComposeView(context, attrs, defStyle) { var text by mutableStateOf("") var onClick by mutableStateOf({}) @Composable override fun Content() { YourAppTheme { CallToActionButton(text, onClick) } } }
Lưu ý rằng các tham số của thành phần kết hợp đó sẽ trở thành biến có thể thay đổi bên trong khung hiển thị tuỳ chỉnh. Nhờ vậy, khung hiển thị CallToActionViewButton
tuỳ chỉnh có thể tăng cường và sử dụng được, như khung hiển thị truyền thống. Hãy xem một ví dụ về trường hợp này với Liên kết khung hiển thị ở bên dưới:
class ViewBindingActivity : ComponentActivity() { private lateinit var binding: ActivityExampleBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityExampleBinding.inflate(layoutInflater) setContentView(binding.root) binding.callToAction.apply { text = getString(R.string.greeting) onClick = { /* Do something */ } } } }
Nếu thành phần tuỳ chỉnh chứa trạng thái có thể thay đổi, hãy xem phần Nguồn trạng thái đáng tin.
Ưu tiên phân chia trạng thái khỏi bản trình bày
Theo truyền thống, View
là một trạng thái. View
quản lý các trường mô tả nội dung cần hiển thị, ngoài cách thức hiển thị nội dung đó. Khi bạn chuyển đổi View
sang Compose, hãy tìm cách tách riêng dữ liệu đang hiển thị sang luồng dữ liệu một chiều, như đã giải thích thêm ở phần chuyển trạng thái lên trên (state hoisting).
Ví dụ: View
có thuộc tính visibility
mô tả liệu thuộc tính này đang hiện, ẩn hay đã biến mất. Đây là một thuộc tính vốn có của View
. Mặc dù các đoạn mã khác có thể làm thay đổi chế độ hiển thị của View
, nhưng bản thân View
chỉ thực sự biết chế độ hiển thị hiện tại của mã đó. Logic để đảm bảo rằng View
hiển thị có thể dễ gặp lỗi và thường liên quan đến chính View
.
Ngược lại, Compose giúp bạn dễ dàng hiển thị các thành phần kết hợp hoàn toàn khác nhau bằng cách sử dụng logic có điều kiện trong Kotlin:
@Composable fun MyComposable(showCautionIcon: Boolean) { if (showCautionIcon) { CautionIcon(/* ... */) } }
Theo thiết kế, CautionIcon
không cần phải biết hoặc quan tâm đến lý do tại sao nội dung đó hiển thị và không có khái niệm về visibility
: nội dung này nằm trong Thành phần kết hợp hoặc không.
Bằng cách tách biệt hoạt động quản lý trạng thái và logic trình bày, bạn có thể tự do thay đổi cách hiển thị nội dung dưới dạng lượt chuyển đổi trạng thái thành giao diện người dùng. Nhờ khả năng chuyển trạng thái lên trên khi cần, bạn cũng có thể sử dụng lại các thành phần kết hợp nhiều lần hơn vì quyền sở hữu trạng thái linh hoạt hơn.
Tăng cấp các thành phần đã đóng gói và có thể sử dụng lại
Các phần tử View
thường biết vị trí tồn tại của chính mình: bên trong Activity
, Dialog
, Fragment
hoặc vị trí nào đó bên trong một hệ phân cấp View
khác. Do những thành phần này thường được tăng cường từ các tệp bố cục tĩnh, nên cấu trúc tổng thể của View
có xu hướng rất cứng nhắc. Điều này dẫn đến việc ghép nối chặt chẽ hơn và khiến View
khó thay đổi hoặc sử dụng lại hơn.
Ví dụ: View
tuỳ chỉnh có thể giả định rằng khung hiển thị này có khung hiển thị con thuộc một loại nhất định với một mã nhận dạng nhất định, đồng thời trực tiếp thay đổi các thuộc tính của loại đó để phản hồi một số hành động. Cách này giúp ghép nối chặt chẽ các phần tử View
đó lại với nhau: thành phần View
tuỳ chỉnh có thể gặp sự cố hoặc bị lỗi nếu không tìm thấy thành phần con và có thể không sử dụng lại được nếu không có thành phần mẹ View
tuỳ chỉnh.
Cách này ít gặp vấn đề hơn trong Compose nhờ các thành phần kết hợp có thể sử dụng lại. Thành phần mẹ có thể dễ dàng chỉ định trạng thái và lệnh gọi lại, nhờ đó, bạn có thể viết các thành phần kết hợp có thể sử dụng lại mà không cần phải biết chính xác vị trí sẽ sử dụng chúng.
@Composable fun AScreen() { var isEnabled by rememberSaveable { mutableStateOf(false) } Column { ImageWithEnabledOverlay(isEnabled) ControlPanelWithToggle( isEnabled = isEnabled, onEnabledChanged = { isEnabled = it } ) } }
Trong ví dụ trên, cả ba phần đều được đóng gói nhiều hơn và ghép nối ít hơn:
ImageWithEnabledOverlay
chỉ cần biết trạng thái hiện tại củaisEnabled
chứ không cần biếtControlPanelWithToggle
có tồn tại hay không hoặc thậm chí là làm thế nào để kiểm soát.ControlPanelWithToggle
không biết rằngImageWithEnabledOverlay
tồn tại. Có thể không có, một hoặc nhiều cách đểisEnabled
hiển thị vàControlPanelWithToggle
sẽ không phải thay đổi.Đối với thành phần mẹ, bạn không cần quan tâm
ImageWithEnabledOverlay
hoặcControlPanelWithToggle
lồng nhau ở mức độ nào. Các thành phần con đó có thể đang tạo ảnh động cho các thay đổi, hoán đổi nội dung hoặc chuyển nội dung cho các thành phần con khác.
Mẫu này còn được gọi là đảo ngược quyền kiểm soát mà bạn có thể đọc thêm trong tài liệu về CompositionLocal
.
Xử lý các thay đổi về kích thước màn hình
Một trong những cách chính để tạo bố cục View
thích ứng là sử dụng nhiều tài nguyên cho các kích thước cửa sổ khác nhau. Mặc dù bạn vẫn có thể lựa chọn các tài nguyên đủ điều kiện khi đưa ra các quyết định về bố cục ở cấp màn hình, nhưng Compose sẽ giúp bạn thay đổi toàn bộ bố cục dễ dàng hơn chỉ bằng mã với logic có điều kiện thông thường. Hãy xem phần Sử dụng lớp kích thước cửa sổ để tìm hiểu thêm.
Ngoài ra, hãy tham khảo bài viết Hỗ trợ nhiều kích thước màn hình để tìm hiểu về các kỹ thuật mà Compose cung cấp để tạo giao diện người dùng thích ứng.
Cuộn dạng lồng với Chế độ xem
Để biết thêm thông tin về cách hỗ trợ khả năng tương tác cuộn dạng lồng giữa các phần tử Khung hiển thị có thể cuộn và thành phần kết hợp có thể cuộn, được lồng theo cả hai hướng, hãy đọc qua phần Khả năng tương tác cuộn dạng lồng.
Compose trong RecyclerView
Các thành phần kết hợp trong RecyclerView
hoạt động hiệu quả kể từ RecyclerView
phiên bản 1.3.0-alpha02. Hãy đảm bảo bạn đang sử dụng ít nhất là phiên bản 1.3.0-alpha02 của RecyclerView
để thấy được những lợi ích đó.
WindowInsets
tương tác với Khung hiển thị
Bạn có thể cần phải ghi đè các phần lồng ghép mặc định khi màn hình có cả Khung hiển thị và mã Compose trong cùng một hệ phân cấp. Trong trường hợp này, bạn cần nêu rõ phần lồng ghép nào sẽ sử dụng và phần lồng ghép nào sẽ bỏ qua.
Ví dụ: nếu bố cục ngoài cùng của bạn là bố cục Khung hiển thị Android, thì bạn nên sử dụng các phần lồng ghép trong hệ thống Khung hiển thị và bỏ qua các phần lồng ghép đó trong Compose.
Ngoài ra, nếu bố cục ngoài cùng của bạn là một thành phần kết hợp, bạn nên sử dụng các phần lồng ghép trong Compose và đệm các thành phần kết hợp AndroidView
cho phù hợp.
Theo mặc định, mỗi ComposeView
sẽ sử dụng tất cả các phần lồng ghép ở mức tiêu thụ WindowInsetsCompat
. Để thay đổi hành vi mặc định này, hãy đặt ComposeView.consumeWindowInsets
thành false
.
Để biết thêm thông tin, hãy đọc tài liệu về WindowInsets
trong Compose.
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Hiển thị biểu tượng cảm xúc
- Material Design 2 trong Compose
- Phần lồng ghép cửa sổ trong Compose