Jetpack Compose được xây dựng dựa trên Kotlin. Trong một số trường hợp, Kotlin cung cấp các thành ngữ đặc biệt giúp bạn viết mã Compose phù hợp dễ dàng hơn. Nếu bạn nghĩ về một ngôn ngữ lập trình khác và chuyển ý ngôn ngữ đó về mặt tinh thần sang Kotlin, thì bạn có thể bỏ lỡ một số điểm mạnh của công cụ Compose. Bạn cũng có thể thấy khó hiểu được cách viết thành ngữ mã Kotlin. Việc càng trở nên quen thuộc hơn với phong cách của Kotlin càng có thể giúp bạn tránh được những sai lầm đó.
Đối số mặc định
Khi viết hàm Kotlin, bạn có thể chỉ định các giá trị mặc định cho các đối số của hàm. Các đối số này được dùng nếu người viết lệnh không chuyển các giá trị đó một cách rõ ràng. Tính năng này làm giảm nhu cầu cần các hàm nạp chồng.
Ví dụ: giả sử bạn muốn viết một hàm vẽ hình vuông. Hàm đó có thể có một thông số bắt buộc duy nhất là sideLength, chỉ định độ dài của mỗi cạnh. Hàm có thể có một vài thông số không bắt buộc, chẳng hạn như thickness, edgeColor, v.v. nếu người viết lệnh không chỉ định các giá trị đó, hàm sẽ sử dụng giá trị mặc định. Ở các ngôn ngữ khác, bạn có thể viết một số hàm:
// We don't need to do this in Kotlin! void drawSquare(int sideLength) { } void drawSquare(int sideLength, int thickness) { } void drawSquare(int sideLength, int thickness, Color edgeColor) { }
Trong Kotlin, bạn có thể viết một hàm duy nhất và chỉ định các giá trị mặc định cho các đối số:
fun drawSquare( sideLength: Int, thickness: Int = 2, edgeColor: Color = Color.Black ) { }
Ngoài việc giúp bạn không phải viết nhiều hàm thừa, tính năng này
giúp mã của bạn dễ đọc hơn. Nếu người viết lệnh không chỉ định
giá trị cho đối số, điều đó cho biết rằng họ sẵn sàng
sử dụng giá trị mặc định. Ngoài ra, các thông số được đặt tên giúp bạn dễ dàng biết điều gì đang
diễn ra. Nếu nhìn vào mã và thấy một lệnh gọi hàm như thế này, thì bạn có thể không
biết ý nghĩa của các thông số nếu không kiểm tra mã drawSquare()
:
drawSquare(30, 5, Color.Red);
Ngược lại, mã này là tự ghi lại là:
drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)
Hầu hết các thư viện của Compose đều sử dụng đối số mặc định. Do đó, bạn nên triển khai tương tự cho các hàm có thể kết hợp mà bạn viết. Phương pháp này giúp bạn có thể tuỳ chỉnh các thành phần kết hợp, nhưng vẫn có thể gọi hoạt động mặc định một cách đơn giản. Ví dụ: bạn có thể tạo một thành phần văn bản đơn giản như sau:
Text(text = "Hello, Android!")
Mã đó có tác dụng giống như mã sau, chi tiết hơn nhiều, trong đó
có nhiều thông số
Text
được đặt rõ ràng hơn:
Text( text = "Hello, Android!", color = Color.Unspecified, fontSize = TextUnit.Unspecified, letterSpacing = TextUnit.Unspecified, overflow = TextOverflow.Clip )
Đoạn mã đầu tiên không chỉ đơn giản và dễ đọc hơn nhiều
mà còn tự ghi lại. Bằng cách chỉ định chỉ thông số text
, bạn ghi nhận lại điều đó cho
tất cả các thông số khác mà bạn muốn sử dụng các giá trị mặc định. Ngược lại, đoạn
mã thứ hai ngụ ý rằng bạn muốn đặt các giá trị cho các thông số khác đó
một cách rõ ràng, mặc dù các giá trị bạn đặt là các giá trị mặc định
cho hàm.
Hàm có thứ tự cao hơn và biểu thức lambda
Kotlin hỗ trợ các hàm
có thứ tự cao hơn, các hàm
nhận các hàm khác làm thông số. Compose xây dựng dựa trên cách tiếp cận này. Ví
dụ: hàm có thể kết hợp
Button
cung cấp tham số lambda onClick
. Giá trị
của thông số đó là một hàm. Nút gọi hàm này khi người dùng nhấp vào hàm:
Button( // ... onClick = myClickFunction ) // ...
Các hàm có thứ tự cao hơn ghép nối tự nhiên với các biểu thức lambda, biểu thức
đánh giá một hàm. Nếu chỉ cần hàm một lần, bạn không cần phải
xác định hàm đó ở nơi khác để chuyển hàm đó đến hàm có thứ tự cao hơn. Thay vào đó, bạn
chỉ cần xác định hàm ngay tại đó bằng một biểu thức lambda. Ví dụ trước
giả định rằng myClickFunction()
được xác định ở nơi khác. Nhưng nếu bạn chỉ sử dụng
hàm đó tại đây, bạn chỉ cần đơn giản là chỉ xác định hàm cùng dòng với biểu thức
lambda:
Button( // ... onClick = { // do something // do something else } ) { /* ... */ }
Các biểu thức lambda tạo vệt
Kotlin cung cấp một cú pháp đặc biệt để gọi các hàm có thứ tự cao hơn có tham số cuối cùng là một biểu thức lambda. Nếu muốn chuyển một biểu thức lambda làm thông số đó, bạn có thể sử dụng cú pháp lambda tạo vệt. Thay vì đặt biểu thức lambda trong dấu ngoặc đơn, bạn đặt biểu thức đó sau đó. Đây là trường hợp phổ biến trong công cụ Compose, vì vậy, bạn cần phải quen thuộc với cách mã trông như thế nào.
Ví dụ: thông số cuối cùng cho tất cả các bố cục, chẳng hạn như
Column()
hàm có thể kết hợp, là content
, một hàm phát ra các thành phần
giao diện người dùng con cháu. Giả sử bạn muốn tạo một cột chứa ba thành phần văn bản
và bạn cần áp dụng một số định dạng. Mã này sẽ hoạt động, nhưng rất
cồng kềnh:
Column( modifier = Modifier.padding(16.dp), content = { Text("Some text") Text("Some more text") Text("Last text") } )
Vì tham số content
là tham số cuối cùng trong chữ ký hàm và
chúng ta đang chuyển giá trị của tham số đó làm một biểu thức lambda, nên chúng ta có thể rút tham số này khỏi
các dấu ngoặc đơn:
Column(modifier = Modifier.padding(16.dp)) { Text("Some text") Text("Some more text") Text("Last text") }
Hai ví dụ này có cùng ý nghĩa. Dấu ngoặc nhọn xác định biểu thức lambda
được chuyển đến thông số content
.
Trên thực tế, nếu thông số only mà bạn đang chuyển là lambda tạo vệt—nghĩa là,
nếu thông số cuối cùng là lambda và bạn không chuyển bất kỳ
thông số nào khác—bạn hoàn toàn có thể bỏ qua các dấu ngoặc đơn. Ví dụ: giả sử bạn
không cần chuyển công cụ sửa đổi đến Column
. Bạn có thể viết mã như
sau:
Column { Text("Some text") Text("Some more text") Text("Last text") }
Cú pháp này khá phổ biến trong công cụ Compose, đặc biệt là đối với các thành phần bố cục như
Column
. Thông số cuối cùng là một biểu thức lambda xác định các thành phần
con cháu của thành phần và những thành phần con cháu đó được chỉ định trong dấu ngoặc nhọn sau lệnh gọi hàm.
Phạm vi và trình nhận
Một số phương thức và thuộc tính chỉ có trong một phạm vi nhất định. Phạm vi giới hạn cho phép bạn cung cấp chức năng ở những nơi cần thiết và tránh vô tình sử dụng chức năng đó ở những nơi không phù hợp.
Hãy xem xét một ví dụ được sử dụng trong công cụ Compose. Khi bạn gọi thành phần kết hợp bố cục Row
biểu thức lambda nội dung của bạn tự động được gọi trong RowScope
.
Việc này sẽ kích hoạt Row
để hiển thị chức năng chỉ có hiệu lực trong Row
.
Ví dụ bên dưới minh họa cách Row
đã hiển thị một giá trị cụ thể theo hàng cho
công cụ sửa đổi align
:
Row { Text( text = "Hello world", // This Text is inside a RowScope so it has access to // Alignment.CenterVertically but not to // Alignment.CenterHorizontally, which would be available // in a ColumnScope. modifier = Modifier.align(Alignment.CenterVertically) ) }
Một số API chấp nhận các biểu thức lambda được gọi trong phạm vi trình nhận. Những lambda đó có quyền truy cập vào các thuộc tính và hàm được xác định ở nơi khác, dựa trên nội dung khai báo thông số:
Box( modifier = Modifier.drawBehind { // This method accepts a lambda of type DrawScope.() -> Unit // therefore in this lambda we can access properties and functions // available from DrawScope, such as the `drawRectangle` function. drawRect( /*...*/ /* ... ) } )
Để biết thêm thông tin, hãy xem các giá trị cố định của hàm có trình nhận trong tài liệu về Kotlin.
Thuộc tính ủy quyền
Kotlin hỗ trợ các thuộc tính
ủy quyền.
Các thuộc tính này được gọi như thể chúng là các trường, nhưng giá trị của các thuộc tính này được
xác định một cách linh động bằng cách đánh giá một biểu thức. Bạn có thể nhận ra các thuộc tính này
bằng việc sử dụng cú pháp của chúngby
:
class DelegatingClass { var name: String by nameGetterFunction() // ... }
Mã khác có thể truy cập vào thuộc tính có mã như sau:
val myDC = DelegatingClass() println("The name property is: " + myDC.name)
Khi println()
thực thi, nameGetterFunction()
được gọi để trả về giá trị
của chuỗi.
Các thuộc tính ủy quyền này đặc biệt hữu ích khi bạn làm việc với các thuộc tính có trạng thái hỗ trợ:
var showDialog by remember { mutableStateOf(false) } // Updating the var automatically triggers a state change showDialog = true
Huỷ cấu trúc lớp dữ liệu
Nếu xác định được một lớp
dữ liệu, bạn có thể dễ dàng
truy cập dữ liệu đó bằng cách khai báo
huỷ cấu trúc. Ví
dụ: giả sử bạn xác định được một lớp Person
:
data class Person(val name: String, val age: Int)
Nếu bạn có một đối tượng thuộc loại đó, bạn có thể truy cập các giá trị của đối tượng bằng mã như sau:
val mary = Person(name = "Mary", age = 35) // ... val (name, age) = mary
Bạn thường sẽ thấy loại mã đó trong các hàm của Compose:
Row { val (image, title, subtitle) = createRefs() // The `createRefs` function returns a data object; // the first three components are extracted into the // image, title, and subtitle variables. // ... }
Lớp dữ liệu cung cấp rất nhiều chức năng hữu ích khác. Ví dụ: khi bạn
xác định một lớp dữ liệu, trình biên dịch sẽ tự động xác định các hàm hữu ích như
equals()
và copy()
. Bạn có thể tìm thêm thông tin trong tài liệu về lớp
dữ liệu.
Đối tượng Singleton
Kotlin giúp bạn dễ dàng khai báo singleton, các lớp luôn có một và
chỉ một phiên bản. Các singleton này được khai báo bằng từ khoá object
.
Compose thường sử dụng những đối tượng như vậy. Ví dụ:
MaterialTheme
được xác định là một đối tượng singleton; tất cả các thuộc tính MaterialTheme.colors
, shapes
và
typography
đều chứa các giá trị cho giao diện hiện tại.
Trình tạo loại an toàn và các DSL
Kotlin cho phép tạo các ngôn ngữ dành riêng cho miền (DSL) bằng các trình tạo loại an toàn. DSL cho phép xây dựng các cấu trúc dữ liệu phân cấp phức tạp theo cách có thể duy trì và dễ đọc hơn.
Jetpack Compose dùng các DSL cho một số API như
LazyRow
và LazyColumn
.
@Composable fun MessageList(messages: List<Message>) { LazyColumn { // Add a single item as a header item { Text("Message List") } // Add list of messages items(messages) { message -> Message(message) } } }
Kotlin đảm bảo các trình tạo loại an toàn sử dụng
các giá trị của hàm có trình nhận.
Nếu chúng ta lấy thành phần kết hợp Canvas
là ví dụ, thì thành phần này được xem là một tham số mà một hàm có
DrawScope
làm trình nhận, onDraw: DrawScope.() -> Unit
, cho phép khối mã
gọi các hàm thành viên được xác định trong DrawScope
.
Canvas(Modifier.size(120.dp)) { // Draw grey background, drawRect function is provided by the receiver drawRect(color = Color.Gray) // Inset content by 10 pixels on the left/right sides // and 12 by the top/bottom inset(10.0f, 12.0f) { val quadrantSize = size / 2.0f // Draw a rectangle within the inset bounds drawRect( size = quadrantSize, color = Color.Red ) rotate(45.0f) { drawRect(size = quadrantSize, color = Color.Blue) } } }
Hãy tìm hiểu thêm về các trình tạo loại an toàn và các DSL trong tài liệu của Kotlin.
Coroutine Kotlin
Coroutine cung cấp dịch vụ hỗ trợ lập trình không đồng bộ ở cấp độ ngôn ngữ trong Kotlin. Coroutine có thể tạm ngưng việc thực thi mà không chặn chuỗi. Một giao diện người dùng thích ứng vốn không đồng bộ và Jetpack Compose sẽ giải quyết vấn đề này bằng cách phối hợp các coroutine ở cấp API thay vì dùng các lệnh gọi lại.
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.
Hàm rememberCoroutineScope
trả về CoroutineScope
mà bạn có thể tạo cáccoroutine trong trình xử lý sự kiện và gọi
Compose tạm ngưng các API. Hãy xem ví dụ bên dưới về việc sử dụng API
animateScrollTo
của
ScrollState
.
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // Create a new coroutine that scrolls to the top of the list // and call the ViewModel to load data composableScope.launch { scrollState.animateScrollTo(0) // This is a suspend function viewModel.loadData() } } ) { /* ... */ }
Theo mặc định, coroutine thực thi khối mã tuần tự. Một
coroutine đang chạy gọi hàm tạm ngưng tạm ngưng việc thực thi cho đến khi
hàm tạm ngưng trả về. Điều này vẫn áp dụng ngay cả khi hàm tạm ngưng
di chuyển tệp thực thi sang một CoroutineDispatcher
khác. Trong ví dụ trước,
loadData
sẽ không được thực thi cho đến khi hàm tạm ngưng animateScrollTo
trả về.
Để thực thi mã đồng thời, bạn cần tạo các coroutine mới. Trong ví dụ
ở trên, để cuộn lên đầu màn hình và tải dữ liệu đồng thời từ
viewModel
, bạn cần sử dụng hai coroutine.
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // Scroll to the top and load data in parallel by creating a new // coroutine per independent work to do composableScope.launch { scrollState.animateScrollTo(0) } composableScope.launch { viewModel.loadData() } } ) { /* ... */ }
Coroutine giúp kết hợp API không đồng bộ dễ dàng hơn. Trong ví dụ
sau, chúng tôi kết hợp công cụ sửa đổi pointerInput
với API ảnh động để
tạo hiệu ứng động vị trí một thành phần khi người dùng nhấn vào màn hình.
@Composable fun MoveBoxWhereTapped() { // Creates an `Animatable` to animate Offset and `remember` it. val animatedOffset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } Box( // The pointerInput modifier takes a suspend block of code Modifier .fillMaxSize() .pointerInput(Unit) { // Create a new CoroutineScope to be able to create new // coroutines inside a suspend function coroutineScope { while (true) { // Wait for the user to tap on the screen val offset = awaitPointerEventScope { awaitFirstDown().position } // Launch a new coroutine to asynchronously animate to // where the user tapped on the screen launch { // Animate to the pressed position animatedOffset.animateTo(offset) } } } } ) { Text("Tap anywhere", Modifier.align(Alignment.Center)) Box( Modifier .offset { // Use the animated offset as the offset of this Box IntOffset( animatedOffset.value.x.roundToInt(), animatedOffset.value.y.roundToInt() ) } .size(40.dp) .background(Color(0xff3c1361), CircleShape) ) }
Để tìm hiểu thêm về Coroutine, hãy xem hướng dẫn về Coroutine của Kotlin trên Android.
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Thành phần và bố cục Material
- Những hiệu ứng phụ trong ứng dụng Compose
- Kiến thức cơ bản về bố cục Compose