expect/actual — bản chất, từ khóa KMM và cách chúng hoạt động

Tác giả: IT Sectr Đã đăng: 2026-06-05 Thời gian đọc: 8 phút

expect/actual là một cơ chế Kotlin Multiplatform cho phép khai báo API phụ thuộc nền tảng trong mã chung. Từ khóa expect tạo một hợp đồng cho hàm, lớp hoặc thuộc tính trong commonMain, trong khi từ khóa actual cung cấp một triển khai cụ thể cho mỗi nền tảng. Trình biên dịch xác minh rằng mỗi khai báo expect có một triển khai actual tương ứng trên tất cả các nền tảng mục tiêu. Theo JetBrains, 2025, cơ chế này được sử dụng trong 80% dự án KMM để triển khai logic nghiệp vụ đa nền tảng.

Ý chính

  • expect là từ khóa để khai báo một hợp đồng của hàm, lớp hoặc thuộc tính trong mã chung.
  • actual là từ khóa để cung cấp một triển khai nền tảng cụ thể của một khai báo expect.
  • commonMain là source set với mã chung nơi các khai báo expect được đặt.
  • Kiểm tra trình biên dịch — trình biên dịch đảm bảo rằng các triển khai actual tồn tại cho tất cả các nền tảng mục tiêu.
  • Source set — các tập hợp (iosMain, androidMain) nơi các triển khai actual cụ thể nền tảng cư trú.

expect/actual là gì?

expect/actual là một cơ chế khai báo của Kotlin Multiplatform để triển khai lập trình hướng nền tảng. Nó cho phép mô tả một API một lần trong mô-đun chung (expect) và triển khai riêng cho từng nền tảng (actual). Không giống như giao diện, expect/actual không tạo lời gọi ảo — trình biên dịch liên kết các khai báo expect và actual tại thời điểm biên dịch, loại bỏ chi phí của điều phối động.

Lịch sử của expect/actual bắt đầu với sự ra đời của Kotlin Multiplatform vào năm 2017. Ban đầu, cơ chế này được gọi là expect/actual declarations và ở giai đoạn thử nghiệm. Trong Kotlin 1.2, các chú thích expect được thêm vào, và trong Kotlin 1.3, expect/actual trở nên ổn định cho các lớp và hàm. Theo thời gian, cơ chế đã được mở rộng: Kotlin 1.6 thêm hỗ trợ expect/actual cho các đối tượng companion, Kotlin 1.7 cho các lớp enum, và Kotlin 2.0 cho typealias.

Tính năng chính của expect/actual là an toàn tại thời điểm biên dịch. Nếu một nhà phát triển thêm một khai báo expect trong commonMain nhưng quên cung cấp triển khai actual cho iOS, trình biên dịch sẽ tạo ra lỗi. Điều này ngăn chặn các lỗi runtime phổ biến trong các phương pháp sử dụng reflection hoặc tải động mã nền tảng.

Cơ chế expect/actual hoạt động như thế nào

Cơ chế của expect/actual hoạt động ở cấp độ source set — hệ thống mô-đun của Kotlin Multiplatform. Mã chung có sẵn cho tất cả các nền tảng nằm trong source set commonMain. Mã phụ thuộc nền tảng nằm trong iosMain, androidMain, macosMain, v.v. Từ khóa expect trong commonMain khai báo một API, trong khi từ khóa actual trong một source set nền tảng cung cấp triển khai. Trình biên dịch liên kết chúng ở giai đoạn tạo mã, thay thế lời gọi hàm expect bằng triển khai actual tương ứng cho nền tảng mục tiêu.

Hệ thống phân cấp source set trong một dự án KMM điển hình như sau: commonMain chứa các khai báo expect, iosMainandroidMain chứa các triển khai actual. Khi biên dịch cho iOS, actual từ iosMain được sử dụng; khi biên dịch cho Android, actual từ androidMain được sử dụng. Các source set có thể là trung gian (ví dụ, iosArm64Main cho một kiến trúc cụ thể), cho phép tinh chỉnh các triển khai cho các thiết bị khác nhau.

kotlin
// commonMain — expect declaration
expect fun getPlatformName(): String

// androidMain — actual for Android
actual fun getPlatformName(): String = "Android"

// iosMain — actual for iOS
actual fun getPlatformName(): String = "iOS"

Kiểm tra triển khai actual bởi trình biên dịch

Trình biên dịch Kotlin kiểm tra một số điều kiện khi làm việc với expect/actual. Mỗi khai báo expect phải có một triển khai actual cho mỗi nền tảng đang hoạt động. Chữ ký của khai báo actual phải khớp với chữ ký expect (chú thích @OptionalExpectation có thể nới lỏng yêu cầu này). Các bộ điều chỉnh truy cập, kiểu trả về và tham số phải giống hệt nhau. Trình biên dịch cũng kiểm tra không có phụ thuộc vòng giữa các khai báo expect và actual.

Các loại expect/actual: hàm, lớp, thuộc tính

expect/actual hỗ trợ một số loại khai báo. Được sử dụng phổ biến nhất là các hàm expect/actual cho các thao tác nền tảng, các lớp expect/actual cho các đối tượng yêu cầu triển khai native, và các thuộc tính expect/actual cho hằng số và cài đặt. Mỗi loại có quy tắc sử dụng và hạn chế riêng.

Các hàm expect/actual là loại đơn giản nhất và phổ biến nhất. Chúng được sử dụng để gọi các API nền tảng như lấy thời gian, đọc tệp hoặc gửi yêu cầu HTTP. Các lớp expect/actual được sử dụng để tạo các đối tượng tương tác trực tiếp với mã native (ví dụ, để truy cập máy ảnh, định vị địa lý hoặc kho lưu trữ khóa). Các thuộc tính expect/actual (val) phù hợp cho các hằng số nền tảng — tên hệ điều hành, phiên bản SDK hoặc đường dẫn thư mục hệ thống.

Loại khai báoTừ khóaVí dụ sử dụng
Hàmexpect fun / actual funLấy mã nhận dạng thiết bị duy nhất
Lớpexpect class / actual classTruy cập SecureStorage (Keychain / EncryptedSharedPreferences)
Thuộc tínhexpect val / actual valNền tảng hiện tại (iOS / Android)
Lớp enumexpect enum / actual enumDanh sách quyền ứng dụng khả dụng
Typealiasexpect typealias / actual typealiasKiểu phản hồi mạng cụ thể nền tảng

Hạn chế của expect/actual

Không phải tất cả cấu trúc Kotlin đều có thể được sử dụng với expect/actual. Một khai báo expect không thể chứa phần thân — chỉ một chữ ký. Một lớp expect không thể có hàm tạo với tham số (phải có hàm tạo chính rỗng). Đối với enum expect/actual, tất cả các hằng số phải giống nhau trong cả expect và actual. Các thuộc tính expect phải là val (không phải var), vì lưu trữ trạng thái trong mô-đun chung cho các thuộc tính nền tảng là vô nghĩa.

Ví dụ mã: từ đơn giản đến phức tạp

Hãy khám phá các ví dụ thực tế của expect/actual từ các hàm đơn giản đến các lớp hoàn chỉnh. Trường hợp cơ bản là lấy tên nền tảng để sử dụng trong giao diện người dùng. Các ví dụ phức tạp hơn bao gồm truy cập bộ nhớ native và làm việc với các luồng nền tảng.

kotlin
// commonMain — expect class for secure storage
expect class PlatformStorage {
    fun save(key: String, value: String)
    fun get(key: String): String?
    fun remove(key: String)
}

// androidMain — actual on Android
actual class PlatformStorage {
    private val prefs = AppContext.getSharedPreferences("secure", 0)

    actual fun save(key: String, value: String) { prefs.edit().putString(key, value).apply() }
    actual fun get(key: String): String? = prefs.getString(key, null)
    actual fun remove(key: String) { prefs.edit().remove(key).apply() }
}

Trong ví dụ này, lớp expect PlatformStorage xác định hợp đồng cho một kho lưu trữ khóa-giá trị đơn giản. Trên Android, triển khai sử dụng SharedPreferences, trong khi trên iOS sử dụng Keychain hoặc NSUserDefaults. Nhờ expect/actual, logic nghiệp vụ trong commonMain gọi save/get/remove mà không cần biết về triển khai nền tảng.

kotlin
// iosMain — actual on iOS with Keychain
actual class PlatformStorage {
    actual fun save(key: String, value: String) {
        val query = mapOf<String, Any>(
            kSecClass to kSecClassGenericPassword,
            kSecAttrAccount to key,
            kSecValueData to value.encodeToByteArray()
        )
        SecItemAdd(query, null)
    }

    actual fun get(key: String): String? {
        val query = mapOf<String, Any>(
            kSecClass to kSecClassGenericPassword,
            kSecAttrAccount to key,
            kSecReturnData to true
        )
        val result = mutableMapOf<String, Any>()
        return if (SecItemCopyMatching(query, result) == errSecSuccess)
            result[kSecValueData]?.toString()
        else null
    }

    actual fun remove(key: String) {
        val query = mapOf<String, Any>(
            kSecClass to kSecClassGenericPassword,
            kSecAttrAccount to key
        )
        SecItemDelete(query)
    }
}

Thực hành tốt nhất với expect/actual

Khi thiết kế API expect/actual, cần tuân theo một số nguyên tắc. Giảm thiểu số lượng khai báo expect — càng có nhiều mã chung, việc bảo trì càng đơn giản. Chỉ sử dụng expect/actual cho các API thực sự khác nhau giữa các nền tảng. Đối với phần còn lại của mã, hãy sử dụng giao diện với các factory hoặc dependency injection, giúp đơn giản hóa việc kiểm thử.

Nên nhóm các khai báo expect theo mô-đun chủ đề, thay vì trộn lẫn chúng trong một tệp duy nhất. Ví dụ, Storage.kt cho các khai báo expect về lưu trữ, Platform.kt cho các hàm expect làm việc với hệ điều hành, và Analytics.kt cho các lớp expect phân tích. Điều này giúp điều hướng và hiểu bề mặt nền tảng của một dự án KMM dễ dàng hơn. Mỗi tệp actual phải nằm trong source set tương ứng: androidMain, iosMain, desktopMain, v.v.

Các triển khai mặc định thông qua expect fun với actual fun nơi actual sử dụng mã chung là một phản mẫu phổ biến. Nếu triển khai nền tảng không khác so với mặc định, thì expect/actual không cần thiết. Trong những trường hợp như vậy, hãy sử dụng một hàm đơn giản trong commonMain. Cũng tránh sử dụng expect/actual cho các getter tầm thường — hãy sử dụng expect val với hằng số.

Tổ chức mã trong dự án

Cấu trúc phù hợp của mã expect/actual rất quan trọng đối với khả năng đọc của dự án. Mỗi mô-đun expect/actual nên có một điểm đầu vào duy nhất. Ví dụ tổ chức: commonMain/kotlin/com/project/platform chứa các khai báo expect, androidMain/kotlin/com/project/platform chứa actual cho Android, iosMain/kotlin/com/project/platform chứa actual cho iOS. Tên tệp và gói phải khớp nhau cho expect và actual, để nhà phát triển có thể nhanh chóng tìm thấy triển khai tương ứng.

Các lựa chọn thay thế cho expect/actual trong KMM

Các giao diện với factory nền tảng là lựa chọn thay thế chính cho expect/actual. Thay vì một lớp expect, bạn có thể khai báo một giao diện trong commonMain và tạo các lớp cụ thể trong các mô-đun nền tảng. Một factory hoặc container dependency injection cung cấp triển khai chính xác tại runtime. Cách tiếp cận này tốt hơn cho việc kiểm thử, vì giao diện có thể được mock.

Dependency Injection (Koin, Kodein) là một cách tiếp cận linh hoạt hơn nhưng kém hiệu quả hơn. Một container DI được cấu hình riêng cho từng nền tảng và cung cấp các phụ thuộc nền tảng cho mã chung. Không giống như expect/actual, việc inject xảy ra tại runtime, cho phép hoán đổi các triển khai để kiểm thử. Mặt khác, các lỗi cấu hình DI chỉ được phát hiện tại runtime, không phải tại thời điểm biên dịch.

Cách tiếp cậnKiểm tra tại biên dịchLinh hoạt kiểm thửChi phí runtime
expect/actualHoàn toànThấp (actual không thể mock)Không (liên kết tại biên dịch)
Giao diện + FactoryMột phầnCao (có thể mock)Tối thiểu (lời gọi ảo)
Dependency InjectionKhông (runtime)CaoTrung bình (proxy DI)

Việc lựa chọn giữa expect/actual và các lựa chọn thay thế phụ thuộc vào ngữ cảnh. Đối với mã quan trọng về hiệu suất (công cụ game, xử lý thời gian thực), expect/actual được ưu tiên do không có chi phí. Đối với logic nghiệp vụ (kho lưu trữ, trường hợp sử dụng), tốt hơn nên sử dụng giao diện với DI để đơn giản hóa việc kiểm thử. Một cách tiếp cận kết hợp — expect/actual cho các thao tác nền tảng cấp thấp và giao diện cho lớp logic nghiệp vụ — được sử dụng trong hầu hết các dự án KMM sản xuất.

Câu hỏi thường gặp

Sự khác biệt giữa expect/actual và giao diện là gì?

expect/actual liên kết triển khai tại thời điểm biên dịch mà không có lời gọi ảo, trong khi giao diện liên kết tại runtime. expect/actual đảm bảo triển khai cho tất cả các nền tảng, giao diện yêu cầu kiểm tra runtime.

Có thể sử dụng expect/actual cho enum không?

Có, expect enum được hỗ trợ từ Kotlin 1.7. Tất cả các hằng số trong enum expect và actual phải khớp nhau. Giá trị hằng số khác nhau trên các nền tảng khác nhau là lỗi biên dịch.

Điều gì xảy ra nếu quên triển khai actual?

Trình biên dịch sẽ tạo lỗi cho mỗi nền tảng thiếu triển khai actual. Dự án sẽ không được xây dựng cho đến khi các triển khai actual tương ứng được thêm vào cho tất cả các khai báo expect.

Có thể sử dụng expect/actual trong cùng một source set không?

Không, expect và actual phải ở các source set khác nhau. expect trong commonMain hoặc source set trung gian, actual trong source set nền tảng. Đặt expect và actual trong cùng một source set là lỗi biên dịch.

Làm thế nào để kiểm thử mã expect/actual?

Để kiểm thử expect/actual, hãy sử dụng commonTest với các source set kiểm thử nền tảng. Viết các bài kiểm thử expect trong commonTest và các bài kiểm thử actual cho từng nền tảng. Các bài kiểm thử tích hợp được chạy riêng trên từng nền tảng mục tiêu.

Tổng kết

  • expect/actual là cơ chế chính của Kotlin Multiplatform cho các triển khai nền tảng với kiểm tra trình biên dịch.
  • expect khai báo một hợp đồng trong commonMain, actual cung cấp triển khai trong source set nền tảng.
  • Các loại khai báo bao gồm hàm, lớp, thuộc tính, lớp enum và typealias với các quy tắc sử dụng khác nhau.
  • Kiểm tra trình biên dịch đảm bảo các triển khai actual cho tất cả các nền tảng mục tiêu, ngăn chặn lỗi runtime.
  • Khuyên giảm thiểu expect/actual và sử dụng giao diện với DI cho logic nghiệp vụ.
  • Tổ chức mã phải nhất quán với tên tệp và gói khớp nhau cho expect và actual.
  • Sử dụng expect/actual cho các thao tác nền tảng cấp thấp (bộ nhớ, hệ thống tệp, cảm biến) — điều này đảm bảo không có chi phí runtime.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm