Sử dụng các mẫu Kotlin thông dụng trên Android

Chủ đề này tập trung vào một số khía cạnh hữu ích nhất của ngôn ngữ Kotlin khi phát triển Android.

Làm việc với fragment (phân đoạn)

Các phần sau đây sử dụng ví dụ về Fragment để làm nổi bật một số tính năng tốt nhất của Kotlin.

Tính kế thừa

Bạn có thể khai báo một lớp trong Kotlin bằng từ khoá class. Trong ví dụ sau, LoginFragment là lớp con của Fragment. Bạn có thể chỉ ra tính kế thừa bằng cách sử dụng toán tử : giữa lớp con và lớp gốc:

class LoginFragment : Fragment()

Trong khai báo về lớp này, LoginFragment chịu trách nhiệm gọi hàm dựng của lớp cấp cao hơn, Fragment.

Trong LoginFragment, bạn có thể ghi đè một số phương thức gọi lại trong vòng đời để phản hồi các thay đổi về trạng thái trong Fragment. Để ghi đè một hàm, hãy sử dụng từ khoá override, như trong ví dụ sau:

override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
): View? {
    return inflater.inflate(R.layout.login_fragment, container, false)
}

Để tham chiếu đến một hàm trong lớp gốc, hãy sử dụng từ khoá super, như trong ví dụ sau:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
}

Tính chất rỗng và thao tác khởi tạo

Trong những ví dụ trước, một số thông số trong các phương thức bị ghi đè có các loại mang hậu tố là dấu chấm hỏi ?. Đây là dấu hiệu cho biết rằng các đối số đã truyền cho các thông số này có thể nhận giá trị rỗng. Hãy đảm bảo bạn xử lý tính chất rỗng một cách an toàn.

Trong Kotlin, bạn phải khởi tạo các thuộc tính của đối tượng khi khai báo đối tượng. Điều này có nghĩa là khi có được thực thể của một lớp, bạn có thể tham chiếu ngay mọi thuộc tính truy cập được của lớp đó. Tuy nhiên, các đối tượng View trong Fragment, chưa sẵn sàng để tăng cường cho đến khi gọi Fragment#onCreateView, vì vậy, bạn cần có một cách để trì hoãn việc khởi tạo thuộc tính cho View.

lateinit cho phép bạn trì hoãn việc khởi tạo thuộc tính. Khi sử dụng lateinit, bạn nên khởi tạo thuộc tính của mình càng sớm càng tốt.

Ví dụ sau minh hoạ việc sử dụng lateinit để gán các đối tượng View trong onViewCreated:

class LoginFragment : Fragment() {

    private lateinit var usernameEditText: EditText
    private lateinit var passwordEditText: EditText
    private lateinit var loginButton: Button
    private lateinit var statusTextView: TextView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        usernameEditText = view.findViewById(R.id.username_edit_text)
        passwordEditText = view.findViewById(R.id.password_edit_text)
        loginButton = view.findViewById(R.id.login_button)
        statusTextView = view.findViewById(R.id.status_text_view)
    }

    ...
}

Chuyển đổi SAM

Bạn có thể theo dõi các sự kiện nhấp chuột trong Android bằng cách triển khai giao diện OnClickListener. Đối tượng Button chứa một hàm setOnClickListener() có mã triển khai của OnClickListener.

OnClickListener có một phương thức đơn trừu tượng là onClick() mà bạn phải triển khai. Vì setOnClickListener() luôn lấy OnClickListener làm đối số và vì OnClickListener luôn có cùng một phương thức đơn trừu tượng, nên cách triển khai này có thể được biểu thị bằng cách sử dụng hàm ẩn danh trong Kotlin. Quá trình này gọi là chuyển đổi phương pháp trừu tượng duy nhất hay còn gọi là chuyển đổi SAM.

Việc chuyển đổi SAM có thể giúp mã của bạn gọn gàng hơn. Ví dụ sau cho biết cách chuyển đổi SAM để triển khai OnClickListener cho Button:

loginButton.setOnClickListener {
    val authSuccessful: Boolean = viewModel.authenticate(
            usernameEditText.text.toString(),
            passwordEditText.text.toString()
    )
    if (authSuccessful) {
        // Navigate to next screen
    } else {
        statusTextView.text = requireContext().getString(R.string.auth_failed)
    }
}

Mã trong hàm ẩn danh được truyền đến setOnClickListener() sẽ thực thi khi người dùng nhấp vào loginButton.

Đối tượng companion (đồng hành)

Đối tượng companion cung cấp cơ chế xác định các biến hoặc hàm được liên kết về mặt khái niệm với một loại nhưng không gắn với một đối tượng cụ thể nào. Đối tượng companion hoạt động tương tự như việc sử dụng từ khoá static của Java cho biến và phương thức.

Trong ví dụ sau, TAG là hằng String. Bạn không cần có một thực thể duy nhất của String cho mỗi thực thể của LoginFragment. Vì vậy, bạn nên xác định nó trong một đối tượng companion:

class LoginFragment : Fragment() {

    ...

    companion object {
        private const val TAG = "LoginFragment"
    }
}

Bạn có thể xác định TAG ở cấp cao nhất của tệp, nhưng tệp đó cũng có thể có nhiều biến, hàm và lớp cũng được xác định ở cấp cao nhất. Các đối tượng companion giúp kết nối các biến, hàm và định nghĩa lớp mà không cần tham chiếu đến bất kỳ thực thể cụ thể nào của lớp đó.

Uỷ quyền thuộc tính

Khi khởi tạo các thuộc tính, bạn có thể lặp lại một số mẫu phổ biến của Android, chẳng hạn như truy cập vào một ViewModel trong Fragment. Để tránh việc có quá nhiều mã trùng lặp, bạn có thể sử dụng cú pháp uỷ quyền thuộc tính của Kotlin.

private val viewModel: LoginViewModel by viewModels()

Cú pháp uỷ quyền thuộc tính cung cấp một mã triển khai phổ biến mà bạn có thể sử dụng lại trong toàn bộ ứng dụng của mình. KTX Android cung cấp cho bạn một số đại biểu thuộc tính cho bạn. Ví dụ: viewModels sẽ truy xuất ViewModel trong phạm vi Fragment hiện tại.

Cú pháp uỷ quyền sử dụng quy trình phản chiếu, làm tiêu tốn thêm tài nguyên. Đổi lại, bạn sẽ có cú pháp ngắn gọn giúp tiết kiệm thời gian phát triển.

Tính chất rỗng

Kotlin cung cấp các quy tắc nghiêm ngặt về tính chất rỗng để duy trì độ an toàn của loại trong toàn bộ ứng dụng của bạn. Theo mặc định, trong Kotlin, các tham chiếu đến đối tượng không thể chứa giá trị rỗng. Để chỉ định giá trị rỗng cho một biến, bạn phải khai báo loại biến có thể nhận giá trị rỗng bằng cách thêm ? vào cuối loại cơ sở.

Ví dụ: biểu thức sau đây là không hợp lệ trong Kotlin. name thuộc loại String và không thể nhận giá trị rỗng:

val name: String = null

Để cho phép giá trị rỗng, bạn phải sử dụng loại String có thể nhận giá trị rỗng, String?, như trong ví dụ sau:

val name: String? = null

Khả năng tương thích

Các quy tắc nghiêm ngặt của Kotlin giúp mã của bạn an toàn và súc tích hơn. Các quy tắc này làm giảm nguy cơ có NullPointerException sẽ khiến ứng dụng của bạn bị lỗi. Ngoài ra, các thuộc tính này giúp giảm số lần kiểm tra giá trị rỗng mà bạn cần thực hiện trong mã.

Thông thường, bạn còn phải gọi mã không phải Kotlin khi viết một ứng dụng Android, vì hầu hết các API của Android đều được viết bằng ngôn ngữ lập trình Java.

Tính chất rỗng là một vấn đề chính mà trong đó, Java và Kotlin có hành vi khác nhau. Java có quy tắc ít nghiêm ngặt hơn về cú pháp tính chất rỗng.

Ví dụ: lớp Account có một vài thuộc tính, trong đó có một thuộc tính String được gọi là name. Java không có quy tắc giống Kotlin về tính chất rỗng, thay vào đó, hãy dựa vào các chú giải tính chất rỗng (không bắt buộc) để khai báo rõ ràng liệu bạn có thể chỉ định giá trị rỗng hay không.

Vì khung Android chủ yếu được viết bằng Java, nên bạn có thể gặp phải trường hợp này khi gọi API mà không có chú giải tính chất rỗng.

Loại nền tảng

Nếu bạn sử dụng Kotlin để tham chiếu một thành phần name chưa chú giải đã được xác định trong một lớp Account trong Java, thì trình biên dịch sẽ không biết liệu String liên kết tới String hay String? trong Kotlin. Tình huống không rõ ràng này được biểu thị qua một loại nền tảngString!.

String! không có ý nghĩa đặc biệt đối với trình biên dịch Kotlin. String! có thể biểu thị String hoặc String?, và trình biên dịch cho phép bạn gán giá trị của một trong hai loại này. Lưu ý rằng có nguy cơ bạn sẽ gửi NullPointerException nếu bạn biểu thị loại này dưới dạng String và chỉ định giá trị rỗng.

Để giải quyết vấn đề này, bạn nên sử dụng chú giải tính chất rỗng bất cứ khi nào bạn viết mã trong Java. Các chú giải này giúp ích cho nhà phát triển của cả Java và Kotlin.

Ví dụ: sau đây là lớp Account như được xác định trong Java:

public class Account implements Parcelable {
    public final String name;
    public final String type;
    private final @Nullable String accessId;

    ...
}

Một trong các biến thành phần (accessId) được chú giải bằng @Nullable để cho biết biến này có thể chứa giá trị rỗng. Sau đó, Kotlin sẽ coi accessIdString?.

Để cho biết rằng một biến không thể có giá trị rỗng, hãy sử dụng chú giải @NonNull:

public class Account implements Parcelable {
    public final @NonNull String name;
    ...
}

Trong trường hợp này, name được coi là String không thể nhận giá trị rỗng trong Kotlin.

Chú giải tính chất rỗng có trong tất cả API mới và nhiều API hiện có của Android. Nhiều thư viện Java đã thêm chú giải tính chất rỗng vào để hỗ trợ tốt hơn cho nhà phát triển trên cả Kotlin và Java.

Xử lý tính chất rỗng

Nếu không chắc chắn về một loại của Java, bạn nên xem loại đó là có thể nhận giá trị rỗng. Ví dụ: thành phần name của lớp Account không được chú thích, do đó, bạn nên mặc định cho rằng đó là một String có thể nhận giá trị rỗng.

Nếu muốn cắt ngắn name để giá trị của nó không bao gồm dấu cách ở đầu hoặc ở cuối, thì bạn có thể sử dụng hàm trim của Kotlin. Bạn có thể cắt ngắn String? theo một số cách an toàn. Một trong những cách này là sử dụng toán tử xác nhận giá trị khác rỗng, !!, như trong ví dụ sau:

val account = Account("name", "type")
val accountName = account.name!!.trim()

Toán tử !! coi tất cả mọi thứ ở bên trái là giá trị khác rỗng, do đó, trong trường hợp này, bạn sẽ coi nameString khác rỗng. Nếu kết quả của biểu thức ở bên trái là giá trị rỗng, thì ứng dụng sẽ gửi một NullPointerException. Toán tử này hoạt động nhanh chóng và dễ dàng, nhưng bạn nên sử dụng một cách thận trọng, vì toán tử này có thể đưa lại các thực thể của NullPointerException vào mã của bạn.

Lựa chọn an toàn hơn là sử dụng toán tử an toàn cho lệnh gọi, ?., như trong ví dụ sau:

val account = Account("name", "type")
val accountName = account.name?.trim()

Sử dụng toán tử an toàn cho lệnh gọi, nếu name khác rỗng, thì kết quả của name?.trim() sẽ là một giá trị tên không có khoảng trắng ở đầu hoặc cuối. Nếu name là giá trị rỗng, thì kết quả của name?.trim() sẽ là null. Điều này có nghĩa là ứng dụng của bạn sẽ không bao giờ gửi NullPointerException khi thực thi câu lệnh này.

Mặc dù toán tử an toàn cho lệnh gọi giúp bạn tránh nguy cơ có NullPointerException, nhưng toán tử này sẽ truyền giá trị rỗng cho câu lệnh tiếp theo. Thay vào đó, bạn có thể xử lý các trường hợp giá trị rỗng ngay lập tức bằng cách sử dụng toán tử Elvis (?:), như trong ví dụ sau:

val account = Account("name", "type")
val accountName = account.name?.trim() ?: "Default name"

Nếu kết quả của biểu thức ở phía bên trái của toán tử Elvis là giá trị rỗng, thì giá trị ở bên phải sẽ được gán cho accountName. Kỹ thuật này rất hữu ích khi cung cấp một giá trị mặc định có thể rỗng.

Bạn cũng có thể sử dụng toán tử Elvis để sớm trả về từ một hàm, như trong ví dụ sau:

fun validateAccount(account: Account?) {
    val accountName = account?.name?.trim() ?: "Default name"

    // account cannot be null beyond this point
    account ?: return

    ...
}

Thay đổi về API của Android

Các API của Android đang ngày càng trở nên tương thích với Kotlin. Nhiều API phổ biến nhất của Android, bao gồm AppCompatActivityFragment, chứa chú giải tính chất rỗng, một số lệnh gọi như Fragment#getContext có lựa chọn thay thế phù hợp cho Kotlin.

Ví dụ: việc truy cập Context của Fragment hầu như luôn có giá trị khác rỗng, vì hầu hết các lệnh gọi bạn thực hiện trong Fragment xảy ra trong khi Fragment được đính kèm với Activity (một lớp con của Context). Tuy nhiên, Fragment#getContext không phải lúc nào cũng trả về giá trị khác rỗng, vì có những trường hợp Fragment không được đính kèm với Activity. Do đó, loại dữ liệu trả về của Fragment#getContext có thể nhận giá trị rỗng.

Context được trả về từ Fragment#getContext không thể nhận giá trị rỗng (và được chú giải là @Nullable), nên bạn phải coi đây là Context? trong mã Kotlin. Như vậy có nghĩa là bạn sẽ áp dụng một trong các toán tử đã đề cập trước đó để xử lý tính chất rỗng trước khi truy cập vào các thuộc tính và hàm của toán tử đó. Đối với một số trường hợp như vậy, Android chứa các API thay thế để mang lại sự tiện lợi này. Ví dụ: Fragment#requireContext trả về một Context khác rỗng và gửi một IllegalStateException nếu được gọi khi Context mang giá trị rỗng. Bằng cách này, bạn có thể coi Context kết quả là giá trị khác rỗng mà không cần sử dụng các toán tử an toàn cho lệnh gọi hoặc giải pháp thay thế.

Khởi tạo thuộc tính

Theo mặc định, các thuộc tính trong Kotlin không được khởi tạo. Bạn phải khởi tạo các thuộc tính này khi khởi tạo lớp bao hàm.

Bạn có thể khởi tạo các thuộc tính theo một vài cách. Ví dụ sau cho thấy cách khởi tạo biến index bằng cách gán giá trị cho biến đó trong phần khai báo của lớp:

class LoginFragment : Fragment() {
    val index: Int = 12
}

Việc khởi tạo này cũng có thể được xác định trong một khối khởi tạo:

class LoginFragment : Fragment() {
    val index: Int

    init {
        index = 12
    }
}

Trong các ví dụ ở trên, index được khởi tạo khi tạo LoginFragment.

Tuy nhiên, có thể bạn sẽ không khởi tạo được một số thuộc tính trong quá trình tạo đối tượng. Ví dụ: Bạn nên tham chiếu đến một View từ bên trong Fragment, nghĩa là trước tiên, bố cục đó phải được tăng cường. Việc tăng cường không xảy ra khi tạo Fragment. Thay vào đó, bố cục sẽ được tăng cường khi gọi Fragment#onCreateView.

Có một cách để giải quyết trường hợp này là khai báo chế độ xem là không thể nhận giá trị rỗng và khởi tạo chế độ xem đó càng sớm càng tốt, như trong ví dụ sau:

class LoginFragment : Fragment() {
    private var statusTextView: TextView? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView?.setText(R.string.auth_failed)
    }
}

Mặc dù cách này có hiệu quả như mong đợi, nhưng giờ đây, bạn phải quản lý tính chất rỗng của View bất cứ khi nào bạn tham chiếu đến thuộc tính này. Một giải pháp hay hơn là dùng lateinit để khởi tạo View, như trong ví dụ sau:

class LoginFragment : Fragment() {
    private lateinit var statusTextView: TextView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView.setText(R.string.auth_failed)
    }
}

Từ khoá lateinit cho phép bạn tránh khởi tạo thuộc tính khi tạo một đối tượng. Nếu thuộc tính của bạn được tham chiếu trước khi được khởi tạo, Kotlin sẽ gửi UninitializedPropertyAccessException, vì vậy, hãy nhớ khởi tạo thuộc tính của bạn càng sớm càng tốt.