Trang này nhằm mục đích hướng dẫn nhà phát triển hiểu rõ các nguyên tắc chung mà Hội đồng API áp dụng trong các quy trình xem xét API.
Ngoài việc tuân thủ các nguyên tắc này khi viết API, nhà phát triển nên chạy công cụ API Lint. Công cụ này mã hoá nhiều quy tắc trong các quy trình kiểm tra mà công cụ chạy đối với API.
Hãy coi đây là hướng dẫn về các quy tắc mà công cụ Lint tuân theo, cộng với lời khuyên chung về những quy tắc không thể được mã hoá vào công cụ đó với độ chính xác cao.
Công cụ API Lint
API Lint được tích hợp vào công cụ phân tích tĩnh Metalava và tự động chạy trong quá trình xác thực trong CI. Bạn có thể chạy thử nghiệm theo cách thủ công từ quy trình thanh toán trên nền tảng cục bộ bằng m
checkapi
hoặc quy trình thanh toán AndroidX cục bộ bằng ./gradlew :path:to:project:checkApi
.
Các quy tắc về API
Nền tảng Android và nhiều thư viện Jetpack đã tồn tại trước khi bộ nguyên tắc này được tạo ra, đồng thời các chính sách được nêu sau đó trên trang này liên tục phát triển để đáp ứng nhu cầu của hệ sinh thái Android.
Do đó, một số API hiện có có thể không tuân thủ các nguyên tắc này. Trong các trường hợp khác, có thể API mới sẽ mang lại trải nghiệm tốt hơn cho người dùng nếu API đó nhất quán với các API hiện có thay vì tuân thủ nghiêm ngặt các nguyên tắc.
Hãy tự đánh giá và liên hệ với Hội đồng API nếu có những câu hỏi khó về một API cần được giải quyết hoặc những nguyên tắc cần được cập nhật.
Thông tin cơ bản về API
Danh mục này liên quan đến các khía cạnh cốt lõi của một API Android.
Bạn phải triển khai tất cả các API
Bất kể đối tượng của API (ví dụ: công khai hoặc @SystemApi
), tất cả các giao diện API phải được triển khai khi hợp nhất hoặc hiển thị dưới dạng API. Không hợp nhất các phần phụ trợ API với việc triển khai sẽ diễn ra vào một ngày khác.
Các nền tảng API không có hoạt động triển khai có nhiều vấn đề:
- Không có gì đảm bảo rằng một nền tảng phù hợp hoặc hoàn chỉnh đã được hiển thị. Cho đến khi một API được kiểm thử hoặc được các ứng dụng sử dụng, không có cách nào để xác minh rằng một ứng dụng có các API phù hợp để có thể sử dụng tính năng này.
- Bạn không thể kiểm thử các API chưa được triển khai trong Bản dùng thử cho nhà phát triển.
- Không thể kiểm thử các API không có quá trình triển khai trong CTS.
Bạn phải kiểm thử tất cả các API
Điều này phù hợp với các yêu cầu CTS của nền tảng, chính sách AndroidX và ý tưởng chung rằng các API phải được triển khai.
Việc kiểm thử các giao diện API đảm bảo rằng giao diện API có thể sử dụng được và chúng tôi đã giải quyết các trường hợp sử dụng dự kiến. Kiểm thử sự tồn tại là không đủ; bạn phải kiểm thử hành vi của chính API.
Thay đổi thêm một API mới phải bao gồm các kiểm thử tương ứng trong cùng một CL hoặc chủ đề Gerrit.
API cũng phải có thể kiểm thử. Bạn phải trả lời được câu hỏi "Nhà phát triển ứng dụng sẽ kiểm thử mã sử dụng API của bạn như thế nào?"
Bạn phải ghi lại tất cả các API
Tài liệu là một phần quan trọng trong khả năng sử dụng API. Mặc dù cú pháp của một giao diện API có vẻ rõ ràng, nhưng mọi ứng dụng mới sẽ không hiểu được ngữ nghĩa, hành vi hoặc ngữ cảnh đằng sau API.
Tất cả các API được tạo phải tuân thủ nguyên tắc
Các API do công cụ tạo phải tuân thủ các nguyên tắc API tương tự như mã được viết tay.
Các công cụ không nên dùng để tạo API:
AutoValue
: vi phạm các nguyên tắc theo nhiều cách, chẳng hạn như không có cách nào để triển khai các lớp giá trị cuối cùng cũng như các trình tạo cuối cùng theo cách mà AutoValue hoạt động.
Kiểu mã
Danh mục này liên quan đến kiểu mã chung mà nhà phát triển nên sử dụng, đặc biệt là khi viết API công khai.
Tuân theo các quy ước lập trình tiêu chuẩn, trừ phi có ghi chú
Các quy ước viết mã Android được ghi lại cho cộng tác viên bên ngoài tại đây:
https://source.android.com/source/code-style.html
Nhìn chung, chúng tôi có xu hướng tuân theo các quy ước lập trình tiêu chuẩn của Java và Kotlin.
Không được viết hoa từ viết tắt trong tên phương thức
Ví dụ: tên phương thức phải là runCtsTests
chứ không phải runCTSTests
.
Tên không được kết thúc bằng Impl
Điều này làm lộ chi tiết triển khai, hãy tránh điều đó.
Lớp
Phần này mô tả các quy tắc về lớp, giao diện và tính kế thừa.
Kế thừa các lớp công khai mới từ lớp cơ sở thích hợp
Tính kế thừa cho thấy các phần tử API trong lớp con của bạn có thể không phù hợp.
Ví dụ: một lớp con công khai mới của FrameLayout
sẽ có dạng FrameLayout
cùng với các hành vi và phần tử API mới. Nếu API được kế thừa đó không phù hợp với trường hợp sử dụng của bạn, hãy kế thừa từ một lớp ở xa hơn trong cây, chẳng hạn như ViewGroup
hoặc View
.
Nếu bạn muốn ghi đè các phương thức từ lớp cơ sở để truyền UnsupportedOperationException
, hãy cân nhắc xem bạn đang sử dụng lớp cơ sở nào.
Sử dụng các lớp cơ sở của bộ sưu tập
Cho dù lấy một tập hợp làm đối số hay trả về tập hợp đó dưới dạng giá trị, hãy luôn ưu tiên lớp cơ sở hơn là việc triển khai cụ thể (chẳng hạn như trả về List<Foo>
thay vì ArrayList<Foo>
).
Sử dụng một lớp cơ sở thể hiện các điều kiện ràng buộc phù hợp cho API. Ví dụ: dùng List
cho một API có bộ sưu tập phải được sắp xếp và dùng Set
cho một API có bộ sưu tập phải bao gồm các phần tử riêng biệt.
Trong Kotlin, hãy ưu tiên các tập hợp không thay đổi được. Hãy xem phần Tính bất biến của tập hợp để biết thêm thông tin chi tiết.
Lớp trừu tượng so với giao diện
Java 8 bổ sung tính năng hỗ trợ cho các phương thức giao diện mặc định, cho phép nhà thiết kế API thêm các phương thức vào giao diện trong khi vẫn duy trì khả năng tương thích nhị phân. Mã nền tảng và tất cả các thư viện Jetpack phải nhắm đến Java 8 trở lên.
Trong trường hợp chế độ triển khai mặc định không trạng thái, nhà thiết kế API nên ưu tiên giao diện hơn các lớp trừu tượng – tức là các phương thức giao diện mặc định có thể được triển khai dưới dạng lệnh gọi đến các phương thức giao diện khác.
Trong trường hợp cần có hàm khởi tạo hoặc trạng thái nội bộ theo cách triển khai mặc định, bạn phải sử dụng các lớp trừu tượng.
Trong cả hai trường hợp, nhà thiết kế API có thể chọn để lại một phương thức trừu tượng duy nhất để đơn giản hoá việc sử dụng dưới dạng một lambda:
public interface AnimationEndCallback {
// Always called, must be implemented.
public void onFinished(Animation anim);
// Optional callbacks.
public default void onStopped(Animation anim) { }
public default void onCanceled(Animation anim) { }
}
Tên lớp phải phản ánh những gì chúng mở rộng
Ví dụ: các lớp mở rộng Service
phải được đặt tên là FooService
để rõ ràng:
public class IntentHelper extends Service {}
public class IntentService extends Service {}
Hậu tố chung
Tránh sử dụng các hậu tố tên lớp chung chung như Helper
và Util
cho các tập hợp phương thức tiện ích. Thay vào đó, hãy đặt các phương thức trực tiếp vào các lớp được liên kết hoặc vào các hàm mở rộng Kotlin.
Trong trường hợp các phương thức đang kết nối nhiều lớp, hãy đặt cho lớp chứa một tên có ý nghĩa để giải thích chức năng của lớp đó.
Trong rất ít trường hợp, bạn có thể sử dụng hậu tố Helper
:
- Được dùng để tạo hành vi mặc định
- Có thể liên quan đến việc uỷ quyền hành vi hiện có cho các lớp mới
- Có thể yêu cầu trạng thái được duy trì
- Thường mất
View
Ví dụ: nếu chú thích backporting yêu cầu duy trì trạng thái được liên kết với một View
và gọi một số phương thức trên View
để cài đặt backport, thì TooltipHelper
sẽ là một tên lớp chấp nhận được.
Đừng trực tiếp hiển thị mã do IDL tạo dưới dạng API công khai
Giữ mã do IDL tạo làm chi tiết triển khai. Trong đó có protobuf, socket, FlatBuffers hoặc bất kỳ bề mặt API nào không phải Java, không phải NDK. Tuy nhiên, hầu hết IDL trong Android đều ở dạng AIDL, vì vậy trang này tập trung vào AIDL.
Các lớp AIDL được tạo không đáp ứng các yêu cầu trong hướng dẫn về kiểu API (ví dụ: các lớp này không thể sử dụng tính năng nạp chồng) và công cụ AIDL không được thiết kế rõ ràng để duy trì khả năng tương thích API ngôn ngữ, vì vậy, bạn không thể nhúng các lớp này vào một API công khai.
Thay vào đó, hãy thêm một lớp API công khai lên trên giao diện AIDL, ngay cả khi ban đầu đó là một trình bao bọc nông.
Giao diện liên kết
Nếu giao diện Binder
là một chi tiết triển khai, thì giao diện này có thể được thay đổi tuỳ ý trong tương lai, với lớp công khai cho phép duy trì khả năng tương thích ngược cần thiết. Ví dụ: bạn có thể cần thêm các đối số mới vào các lệnh gọi nội bộ hoặc tối ưu hoá lưu lượng truy cập IPC bằng cách sử dụng tính năng xử lý hàng loạt hoặc truyền phát trực tiếp, sử dụng bộ nhớ dùng chung hoặc các phương pháp tương tự. Bạn không thể thực hiện bất kỳ thao tác nào trong số này nếu giao diện AIDL của bạn cũng là API công khai.
Ví dụ: đừng trực tiếp hiển thị FooService
dưới dạng API công khai:
// BAD: Public API generated from IFooService.aidl
public class IFooService {
public void doFoo(String foo);
}
Thay vào đó, hãy bao bọc giao diện Binder
bên trong một trình quản lý hoặc lớp khác:
/**
* @hide
*/
public class IFooService {
public void doFoo(String foo);
}
public IFooManager {
public void doFoo(String foo) {
mFooService.doFoo(foo);
}
}
Nếu sau này cần một đối số mới cho lệnh gọi này, thì giao diện nội bộ có thể là các phương thức nạp chồng tối thiểu và thuận tiện được thêm vào API công khai. Bạn có thể sử dụng lớp bao bọc để xử lý các vấn đề khác về khả năng tương thích ngược khi quá trình triển khai phát triển:
/**
* @hide
*/
public class IFooService {
public void doFoo(String foo, int flags);
}
public IFooManager {
public void doFoo(String foo) {
if (mAppTargetSdkLevel < 26) {
useOldFooLogic(); // Apps targeting API before 26 are broken otherwise
mFooService.doFoo(foo, FLAG_THAT_ONE_WEIRD_HACK);
} else {
mFooService.doFoo(foo, 0);
}
}
public void doFoo(String foo, int flags) {
mFooService.doFoo(foo, flags);
}
}
Đối với các giao diện Binder
không thuộc nền tảng Android (ví dụ: giao diện dịch vụ do Dịch vụ Google Play xuất cho các ứng dụng sử dụng), yêu cầu về giao diện IPC ổn định, đã xuất bản và có phiên bản có nghĩa là sẽ khó phát triển chính giao diện đó hơn nhiều. Tuy nhiên, bạn vẫn nên có một lớp trình bao bọc xung quanh lớp này để khớp với các nguyên tắc API khác và giúp bạn dễ dàng sử dụng cùng một API công khai cho phiên bản mới của giao diện IPC, nếu cần.
Không sử dụng các đối tượng Binder thô trong API công khai
Bản thân đối tượng Binder
không có ý nghĩa gì và do đó, không nên dùng trong API công khai. Một trường hợp sử dụng phổ biến là dùng Binder
hoặc IBinder
làm mã thông báo vì mã này có ngữ nghĩa nhận dạng. Thay vì sử dụng đối tượng Binder
thô, hãy sử dụng một lớp mã thông báo trình bao bọc.
public final class IdentifiableObject {
public Binder getToken() {...}
}
public final class IdentifiableObjectToken {
/**
* @hide
*/
public Binder getRawValue() {...}
/**
* @hide
*/
public static IdentifiableObjectToken wrapToken(Binder rawValue) {...}
}
public final class IdentifiableObject {
public IdentifiableObjectToken getToken() {...}
}
Các lớp trình quản lý phải là lớp cuối cùng
Các lớp Trình quản lý phải được khai báo là final
. Các lớp Trình quản lý giao tiếp với các dịch vụ hệ thống và là điểm tương tác duy nhất. Bạn không cần tuỳ chỉnh nên hãy khai báo nó là final
.
Không sử dụng CompletableFuture hoặc Future
java.util.concurrent.CompletableFuture
có một bề mặt API lớn cho phép biến đổi tuỳ ý giá trị của tương lai và có các giá trị mặc định dễ xảy ra lỗi.
Ngược lại, java.util.concurrent.Future
thiếu tính năng nghe không chặn, khiến khó sử dụng với mã không đồng bộ.
Trong mã nền tảng và API thư viện cấp thấp mà cả Kotlin và Java đều sử dụng, hãy ưu tiên kết hợp lệnh gọi lại hoàn tất, Executor
và nếu API hỗ trợ việc huỷ CancellationSignal
.
public void asyncLoadFoo(android.os.CancellationSignal cancellationSignal,
Executor callbackExecutor,
android.os.OutcomeReceiver<FooResult, Throwable> callback);
Nếu bạn đang nhắm đến Kotlin, hãy ưu tiên các hàm suspend
.
suspend fun asyncLoadFoo(): Foo
Trong các thư viện tích hợp dành riêng cho Java, bạn có thể sử dụng ListenableFuture
của Guava.
public com.google.common.util.concurrent.ListenableFuture<Foo> asyncLoadFoo();
Không sử dụng tuỳ chọn
Mặc dù Optional
có thể có lợi thế ở một số khu vực API, nhưng nó không nhất quán với khu vực API Android hiện có. @Nullable
và @NonNull
cung cấp sự hỗ trợ về công cụ cho độ an toàn null
và Kotlin thực thi các hợp đồng về khả năng có giá trị rỗng ở cấp trình biên dịch, khiến Optional
trở nên không cần thiết.
Đối với các nguyên hàm không bắt buộc, hãy sử dụng các phương thức has
và get
được ghép nối. Nếu giá trị không được đặt (has
trả về false
), phương thức get
sẽ gửi một IllegalStateException
.
public boolean hasAzimuth() { ... }
public int getAzimuth() {
if (!hasAzimuth()) {
throw new IllegalStateException("azimuth is not set");
}
return azimuth;
}
Sử dụng hàm khởi tạo riêng tư cho các lớp không thể khởi tạo
Các lớp chỉ có thể được tạo bởi Builder
, các lớp chỉ chứa hằng số hoặc phương thức tĩnh, hoặc các lớp không thể khởi tạo khác phải có ít nhất một hàm khởi tạo riêng tư để ngăn việc khởi tạo bằng hàm khởi tạo mặc định không có đối số.
public final class Log {
// Not instantiable.
private Log() {}
}
Singleton
Bạn không nên dùng singleton vì chúng có những nhược điểm sau đây liên quan đến việc kiểm thử:
- Việc tạo được quản lý theo lớp, ngăn chặn việc sử dụng các giá trị giả
- Không thể kiểm thử khép kín do bản chất tĩnh của một singleton
- Để giải quyết những vấn đề này, nhà phát triển phải biết thông tin chi tiết nội bộ của singleton hoặc tạo một trình bao bọc xung quanh singleton
Ưu tiên mẫu single instance (một thực thể duy nhất), dựa vào một lớp cơ sở trừu tượng để giải quyết những vấn đề này.
Một phiên bản
Các lớp có một thực thể sử dụng một lớp cơ sở trừu tượng có hàm khởi tạo private
hoặc internal
và cung cấp một phương thức tĩnh getInstance()
để lấy một thực thể. Phương thức getInstance()
phải trả về cùng một đối tượng trong các lệnh gọi tiếp theo.
Đối tượng do getInstance()
trả về phải là một cách triển khai riêng của lớp cơ sở trừu tượng.
class Singleton private constructor(...) {
companion object {
private val _instance: Singleton by lazy { Singleton(...) }
fun getInstance(): Singleton {
return _instance
}
}
}
abstract class SingleInstance private constructor(...) {
companion object {
private val _instance: SingleInstance by lazy { SingleInstanceImp(...) }
fun getInstance(): SingleInstance {
return _instance
}
}
}
Một thực thể khác với singleton ở chỗ nhà phát triển có thể tạo phiên bản giả của SingleInstance
và sử dụng khung Tiêm phần phụ thuộc của riêng họ để quản lý việc triển khai mà không cần tạo trình bao bọc, hoặc thư viện có thể cung cấp phiên bản giả của riêng mình trong một cấu phần phần mềm -testing
.
Các lớp phát hành tài nguyên phải triển khai AutoCloseable
Các lớp phát hành tài nguyên thông qua close
, release
, destroy
hoặc các phương thức tương tự phải triển khai java.lang.AutoCloseable
để cho phép nhà phát triển tự động dọn dẹp các tài nguyên này khi sử dụng một khối try-with-resources
.
Tránh giới thiệu các lớp con View mới trong android.*
Không giới thiệu các lớp mới kế thừa trực tiếp hoặc gián tiếp từ android.view.View
trong API công khai của nền tảng (tức là trong android.*
).
Bộ công cụ giao diện người dùng của Android hiện là Compose-first. Các tính năng mới trên giao diện người dùng do nền tảng cung cấp phải được cung cấp dưới dạng các API cấp thấp có thể dùng để triển khai Jetpack Compose và các thành phần giao diện người dùng dựa trên Khung hiển thị (không bắt buộc) cho nhà phát triển trong các thư viện Jetpack. Việc cung cấp các thành phần này trong thư viện mang đến cơ hội cho các hoạt động triển khai được chuyển ngược khi các tính năng của nền tảng không có sẵn.
Trường
Đây là các quy tắc về trường công khai trên các lớp.
Không hiển thị các trường thô
Các lớp Java không được trực tiếp hiển thị các trường. Các trường phải là trường riêng tư và chỉ có thể truy cập bằng cách sử dụng các phương thức truy xuất và thiết lập công khai, bất kể các trường này có phải là trường cuối cùng hay không.
Các trường hợp ngoại lệ hiếm gặp bao gồm các cấu trúc dữ liệu cơ bản mà không cần nâng cao hành vi chỉ định hoặc truy xuất một trường. Trong những trường hợp như vậy, bạn nên đặt tên cho các trường bằng cách sử dụng quy ước đặt tên biến tiêu chuẩn, ví dụ: Point.x
và Point.y
.
Các lớp Kotlin có thể hiển thị các thuộc tính.
Bạn nên đánh dấu các trường được hiển thị là trường cuối cùng
Bạn không nên sử dụng các trường thô (@see Đừng để lộ các trường thô). Nhưng trong trường hợp hiếm gặp khi một trường được hiển thị dưới dạng trường công khai, hãy đánh dấu trường đó là final
.
Không nên hiển thị các trường nội bộ
Không tham chiếu tên trường nội bộ trong API công khai.
public int mFlags;
Sử dụng công khai thay vì được bảo vệ
@see Sử dụng public thay vì protected
Hằng số
Đây là các quy tắc về hằng số công khai.
Hằng số cờ không được trùng lặp các giá trị int hoặc giá trị dài
Cờ ngụ ý các bit có thể kết hợp thành một số giá trị hợp nhất. Nếu không phải như vậy, đừng gọi biến hoặc hằng số flag
.
public static final int FLAG_SOMETHING = 2;
public static final int FLAG_SOMETHING = 3;
public static final int FLAG_PRIVATE = 1 << 2;
public static final int FLAG_PRESENTATION = 1 << 3;
Hãy xem @IntDef
cho các cờ bitmask để biết thêm thông tin về cách xác định các hằng số cờ công khai.
các hằng số tĩnh cuối cùng phải sử dụng quy ước đặt tên bằng chữ in hoa, phân tách bằng dấu gạch dưới
Tất cả các từ trong hằng số phải được viết hoa và nhiều từ phải được phân tách bằng _
. Ví dụ:
public static final int fooThing = 5
public static final int FOO_THING = 5
Sử dụng tiền tố chuẩn cho hằng số
Nhiều hằng số được dùng trong Android là cho những thứ tiêu chuẩn, chẳng hạn như cờ, khoá và thao tác. Các hằng số này phải có tiền tố tiêu chuẩn để giúp bạn dễ dàng xác định chúng hơn.
Ví dụ: các phần bổ sung về ý định phải bắt đầu bằng EXTRA_
. Các thao tác theo ý định phải bắt đầu bằng ACTION_
. Các hằng số được dùng với Context.bindService()
phải bắt đầu bằng BIND_
.
Tên và phạm vi hằng số chính
Các giá trị hằng số chuỗi phải nhất quán với chính tên hằng số và thường được giới hạn trong gói hoặc miền. Ví dụ:
public static final String FOO_THING = "foo"
không được đặt tên nhất quán cũng như không được đặt phạm vi thích hợp. Thay vào đó, hãy cân nhắc:
public static final String FOO_THING = "android.fooservice.FOO_THING"
Tiền tố của android
trong các hằng số chuỗi có phạm vi được dành riêng cho Dự án nguồn mở Android.
Các thao tác và phần bổ sung của ý định, cũng như các mục trong Bundle, phải được đặt tên theo không gian tên bằng tên gói mà chúng được xác định trong đó.
package android.foo.bar {
public static final String ACTION_BAZ = "android.foo.bar.action.BAZ"
public static final String EXTRA_BAZ = "android.foo.bar.extra.BAZ"
}
Sử dụng công khai thay vì được bảo vệ
@see Sử dụng public thay vì protected
Sử dụng tiền tố nhất quán
Tất cả hằng số có liên quan đều phải bắt đầu bằng cùng một tiền tố. Ví dụ: đối với một tập hợp hằng số để dùng với các giá trị cờ:
public static final int SOME_VALUE = 0x01;
public static final int SOME_OTHER_VALUE = 0x10;
public static final int SOME_THIRD_VALUE = 0x100;
public static final int FLAG_SOME_VALUE = 0x01;
public static final int FLAG_SOME_OTHER_VALUE = 0x10;
public static final int FLAG_SOME_THIRD_VALUE = 0x100;
@see Sử dụng tiền tố chuẩn cho hằng số
Sử dụng tên tài nguyên nhất quán
Giá trị nhận dạng, thuộc tính và giá trị công khai phải được đặt tên theo quy ước đặt tên camelCase, ví dụ: @id/accessibilityActionPageUp
hoặc @attr/textAppearance
, tương tự như các trường công khai trong Java.
Trong một số trường hợp, giá trị nhận dạng hoặc thuộc tính công khai có một tiền tố chung được phân tách bằng dấu gạch dưới:
- Các giá trị cấu hình nền tảng, chẳng hạn như
@string/config_recentsComponentName
trong config.xml - Các thuộc tính của thành phần hiển thị dành riêng cho bố cục, chẳng hạn như
@attr/layout_marginStart
trong attrs.xml
Các giao diện và kiểu công khai phải tuân theo quy ước đặt tên theo quy tắc PascalCase phân cấp, ví dụ: @style/Theme.Material.Light.DarkActionBar
hoặc @style/Widget.Material.SearchView.ActionBar
, tương tự như các lớp lồng nhau trong Java.
Không nên hiển thị bố cục và tài nguyên có thể vẽ dưới dạng API công khai. Tuy nhiên, nếu phải hiển thị, thì bố cục và các thành phần có thể vẽ công khai phải được đặt tên theo quy ước đặt tên under_score, ví dụ: layout/simple_list_item_1.xml
hoặc drawable/title_bar_tall.xml
.
Khi hằng số có thể thay đổi, hãy tạo hằng số động
Trình biên dịch có thể nội tuyến các giá trị hằng số, vì vậy, việc giữ nguyên các giá trị được coi là một phần của hợp đồng API. Nếu giá trị của hằng số MIN_FOO
hoặc MAX_FOO
có thể thay đổi trong tương lai, hãy cân nhắc việc biến chúng thành các phương thức động.
CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()
Cân nhắc khả năng tương thích chuyển tiếp cho các lệnh gọi lại
Các hằng số được xác định trong các phiên bản API sau này không được các ứng dụng nhắm đến các API cũ hơn biết đến. Vì lý do này, các hằng số được gửi đến ứng dụng phải xem xét phiên bản API mục tiêu của ứng dụng đó và ánh xạ các hằng số mới hơn thành một giá trị nhất quán. Hãy xem xét trường hợp sau:
Nguồn SDK giả định:
// Added in API level 22
public static final int STATUS_SUCCESS = 1;
public static final int STATUS_FAILURE = 2;
// Added in API level 23
public static final int STATUS_FAILURE_RETRY = 3;
// Added in API level 26
public static final int STATUS_FAILURE_ABORT = 4;
Ứng dụng giả định có targetSdkVersion="22"
:
if (result == STATUS_FAILURE) {
// Oh no!
} else {
// Success!
}
Trong trường hợp này, ứng dụng được thiết kế trong phạm vi hạn chế của API cấp 22 và đưa ra một giả định (phần nào) hợp lý rằng chỉ có 2 trạng thái có thể xảy ra. Tuy nhiên, nếu nhận được STATUS_FAILURE_RETRY
mới thêm, ứng dụng sẽ diễn giải đây là thành công.
Các phương thức trả về hằng số có thể xử lý an toàn các trường hợp như thế này bằng cách hạn chế đầu ra của chúng khớp với cấp độ API mà ứng dụng nhắm đến:
private int mapResultForTargetSdk(Context context, int result) {
int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
if (targetSdkVersion < 26) {
if (result == STATUS_FAILURE_ABORT) {
return STATUS_FAILURE;
}
if (targetSdkVersion < 23) {
if (result == STATUS_FAILURE_RETRY) {
return STATUS_FAILURE;
}
}
}
return result;
}
Nhà phát triển không thể dự đoán liệu danh sách hằng số có thể thay đổi trong tương lai hay không. Nếu bạn xác định một API bằng hằng số UNKNOWN
hoặc UNSPECIFIED
có vẻ như là một hằng số chung, thì các nhà phát triển sẽ giả định rằng các hằng số đã xuất bản khi họ viết ứng dụng của mình là đầy đủ. Nếu bạn không muốn đặt kỳ vọng này, hãy cân nhắc xem hằng số chung có phải là ý tưởng hay cho API của bạn hay không.
Ngoài ra, các thư viện không thể chỉ định targetSdkVersion
riêng biệt với ứng dụng và việc xử lý các thay đổi về hành vi targetSdkVersion
từ mã thư viện rất phức tạp và dễ xảy ra lỗi.
Hằng số số nguyên hoặc chuỗi
Sử dụng hằng số số nguyên và @IntDef
nếu không gian tên cho các giá trị không thể mở rộng bên ngoài gói của bạn. Sử dụng hằng số chuỗi nếu không gian tên được chia sẻ hoặc có thể được mở rộng bằng mã bên ngoài gói của bạn.
Lớp dữ liệu
Lớp dữ liệu đại diện cho một tập hợp các thuộc tính bất biến và cung cấp một tập hợp nhỏ và được xác định rõ các hàm tiện ích để tương tác với dữ liệu đó.
Không dùng data class
trong các API Kotlin công khai, vì trình biên dịch Kotlin không đảm bảo khả năng tương thích API ngôn ngữ hoặc tệp nhị phân cho mã đã tạo. Thay vào đó, hãy triển khai các hàm bắt buộc theo cách thủ công.
Tạo bản sao
Trong Java, các lớp dữ liệu phải cung cấp một hàm khởi tạo khi có ít thuộc tính hoặc sử dụng mẫu Builder
khi có nhiều thuộc tính.
Trong Kotlin, các lớp dữ liệu phải cung cấp một hàm khởi tạo có các đối số mặc định, bất kể số lượng thuộc tính. Các lớp dữ liệu được xác định trong Kotlin cũng có thể hưởng lợi từ việc cung cấp một trình tạo khi nhắm đến các ứng dụng Java.
Sửa đổi và sao chép
Trong trường hợp cần sửa đổi dữ liệu, hãy cung cấp một lớp Builder
có hàm sao chép (Java) hoặc một hàm thành phần copy()
(Kotlin) trả về một đối tượng mới.
Khi cung cấp một hàm copy()
trong Kotlin, các đối số phải khớp với hàm khởi tạo của lớp và các giá trị mặc định phải được điền bằng các giá trị hiện tại của đối tượng:
class Typography(
val labelMedium: TextStyle = TypographyTokens.LabelMedium,
val labelSmall: TextStyle = TypographyTokens.LabelSmall
) {
fun copy(
labelMedium: TextStyle = this.labelMedium,
labelSmall: TextStyle = this.labelSmall
): Typography = Typography(
labelMedium = labelMedium,
labelSmall = labelSmall
)
}
Các hành vi khác
Các lớp dữ liệu phải triển khai cả equals()
và hashCode()
, đồng thời mọi thuộc tính phải được tính đến trong quá trình triển khai các phương thức này.
Các lớp dữ liệu có thể triển khai toString()
bằng định dạng được đề xuất khớp với việc triển khai lớp dữ liệu của Kotlin, ví dụ: User(var1=Alex, var2=42)
.
Phương thức
Đây là các quy tắc về nhiều điểm cụ thể trong các phương thức, xung quanh các tham số, tên phương thức, kiểu dữ liệu trả về và bộ chỉ định quyền truy cập.
Giờ
Các quy tắc này bao gồm cách thể hiện các khái niệm về thời gian như ngày và khoảng thời gian trong API.
Ưu tiên các loại java.time.* nếu có thể
java.time.Duration
, java.time.Instant
và nhiều loại java.time.*
khác có trên tất cả các phiên bản nền tảng thông qua desugaring và nên được ưu tiên khi biểu thị thời gian trong các tham số API hoặc giá trị trả về.
Chỉ nên hiển thị các biến thể của một API chấp nhận hoặc trả về java.time.Duration
hoặc java.time.Instant
và bỏ qua các biến thể nguyên thuỷ có cùng trường hợp sử dụng, trừ phi miền API là một miền mà việc phân bổ đối tượng trong các mẫu sử dụng dự kiến sẽ có tác động tiêu cực đến hiệu suất.
Các phương thức thể hiện thời lượng phải được đặt tên là duration
Nếu giá trị thời gian biểu thị khoảng thời gian liên quan, hãy đặt tên cho tham số là "duration" (thời lượng) chứ không phải "time" (thời gian).
ValueAnimator.setTime(java.time.Duration);
ValueAnimator.setDuration(java.time.Duration);
Trường hợp ngoại lệ:
"timeout" phù hợp khi khoảng thời gian áp dụng cụ thể cho giá trị thời gian chờ.
"time" có loại java.time.Instant
là phù hợp khi đề cập đến một thời điểm cụ thể, chứ không phải khoảng thời gian.
Các phương thức biểu thị khoảng thời gian hoặc thời gian dưới dạng một kiểu dữ liệu nguyên thuỷ phải được đặt tên theo đơn vị thời gian và sử dụng kiểu dữ liệu long
Các phương thức chấp nhận hoặc trả về khoảng thời gian dưới dạng một kiểu dữ liệu nguyên thuỷ phải thêm hậu tố cho tên phương thức bằng các đơn vị thời gian liên kết (chẳng hạn như Millis
, Nanos
, Seconds
) để dành tên không được trang trí cho việc sử dụng với java.time.Duration
. Xem phần Thời gian.
Bạn cũng nên chú thích các phương thức một cách phù hợp bằng đơn vị và cơ sở thời gian của chúng:
@CurrentTimeMillisLong
: Giá trị là một dấu thời gian không âm, được đo bằng số mili giây kể từ 1970-01-01T00:00:00Z.@CurrentTimeSecondsLong
: Giá trị là một dấu thời gian không âm, được đo bằng số giây kể từ 1970-01-01T00:00:00Z.@DurationMillisLong
: Giá trị là thời lượng không âm tính bằng mili giây.@ElapsedRealtimeLong
: Giá trị là dấu thời gian không âm trong cơ sở thời gianSystemClock.elapsedRealtime()
.@UptimeMillisLong
: Giá trị là dấu thời gian không âm trong cơ sở thời gianSystemClock.uptimeMillis()
.
Các tham số thời gian hoặc giá trị trả về nguyên thuỷ phải sử dụng long
, chứ không phải int
.
ValueAnimator.setDuration(@DurationMillisLong long);
ValueAnimator.setDurationNanos(long);
Các phương thức thể hiện đơn vị thời gian nên ưu tiên cách viết tắt không rút gọn cho tên đơn vị
public void setIntervalNs(long intervalNs);
public void setTimeoutUs(long timeoutUs);
public void setIntervalNanos(long intervalNanos);
public void setTimeoutMicros(long timeoutMicros);
Chú thích các đối số thời gian dài
Nền tảng này có một số chú thích để cung cấp khả năng nhập mạnh hơn cho các đơn vị thời gian thuộc loại long
:
@CurrentTimeMillisLong
: Giá trị là dấu thời gian không âm được đo bằng số mili giây kể từ1970-01-01T00:00:00Z
, do đó, trong cơ sở thời gianSystem.currentTimeMillis()
.@CurrentTimeSecondsLong
: Giá trị là một dấu thời gian không âm, được đo bằng số giây kể từ1970-01-01T00:00:00Z
.@DurationMillisLong
: Giá trị là thời lượng không âm tính bằng mili giây.@ElapsedRealtimeLong
: Giá trị là dấu thời gian không âm trong cơ sở thời gianSystemClock#elapsedRealtime()
.@UptimeMillisLong
: Giá trị là dấu thời gian không âm trong cơ sở thời gianSystemClock#uptimeMillis()
.
Đơn vị đo lường
Đối với tất cả các phương thức thể hiện một đơn vị đo lường khác với thời gian, hãy ưu tiên tiền tố đơn vị SI theo kiểu CamelCase.
public long[] getFrequenciesKhz();
public float getStreamVolumeDb();
Đặt các tham số không bắt buộc ở cuối các phương thức nạp chồng
Nếu bạn có các phương thức nạp chồng có tham số không bắt buộc, hãy giữ các tham số đó ở cuối và duy trì thứ tự nhất quán với các tham số khác:
public int doFoo(boolean flag);
public int doFoo(int id, boolean flag);
public int doFoo(boolean flag);
public int doFoo(boolean flag, int id);
Khi thêm các phương thức nạp chồng cho các đối số không bắt buộc, hành vi của các phương thức đơn giản hơn phải hoạt động theo cách giống hệt như khi các đối số mặc định được cung cấp cho các phương thức phức tạp hơn.
Hệ quả: Không nên nạp chồng các phương thức, trừ phi để thêm đối số không bắt buộc hoặc chấp nhận các loại đối số khác nhau nếu phương thức là đa hình. Nếu phương thức bị nạp chồng làm một việc gì đó khác biệt về cơ bản, thì hãy đặt cho phương thức đó một tên mới.
Các phương thức có tham số mặc định phải được chú thích bằng @JvmOverloads (chỉ Kotlin)
Các phương thức và hàm khởi tạo có tham số mặc định phải được chú thích bằng @JvmOverloads
để duy trì khả năng tương thích nhị phân.
Hãy xem phần Nạp chồng hàm cho các giá trị mặc định trong hướng dẫn chính thức về khả năng tương tác Kotlin-Java để biết thêm thông tin.
class Greeting @JvmOverloads constructor(
loudness: Int = 5
) {
@JvmOverloads
fun sayHello(prefix: String = "Dr.", name: String) = // ...
}
Không xoá các giá trị tham số mặc định (chỉ Kotlin)
Nếu một phương thức được phân phối cùng với một tham số có giá trị mặc định, thì việc xoá giá trị mặc định là một thay đổi làm gián đoạn nguồn.
Các thông số phương thức nhận dạng và đặc biệt nhất phải được đặt trước
Nếu bạn có một phương thức có nhiều tham số, hãy đặt các tham số phù hợp nhất lên trước. Các tham số chỉ định cờ và các lựa chọn khác ít quan trọng hơn những tham số mô tả đối tượng đang được tác động. Nếu có lệnh gọi lại hoàn tất, hãy đặt lệnh gọi lại đó ở cuối.
public void openFile(int flags, String name);
public void openFileAsync(OnFileOpenedListener listener, String name, int flags);
public void setFlags(int mask, int flags);
public void openFile(String name, int flags);
public void openFileAsync(String name, int flags, OnFileOpenedListener listener);
public void setFlags(int flags, int mask);
Xem thêm: Đặt các tham số không bắt buộc ở cuối trong các phương thức nạp chồng
Công ty xây dựng
Bạn nên dùng mẫu Trình tạo để tạo các đối tượng Java phức tạp và thường dùng mẫu này trong Android cho các trường hợp sau:
- Các thuộc tính của đối tượng kết quả phải là bất biến
- Có rất nhiều thuộc tính bắt buộc, chẳng hạn như nhiều đối số của hàm khởi tạo
- Có mối quan hệ phức tạp giữa các thuộc tính tại thời điểm tạo, ví dụ: cần có một bước xác minh. Xin lưu ý rằng mức độ phức tạp này thường cho thấy các vấn đề về khả năng sử dụng của API.
Hãy cân nhắc xem bạn có cần một nhà thầu hay không. Trình tạo sẽ hữu ích trong một giao diện API nếu được dùng để:
- Chỉ định cấu hình một số ít trong số nhiều tham số tạo không bắt buộc
- Định cấu hình nhiều tham số tạo bắt buộc hoặc không bắt buộc khác nhau, đôi khi thuộc các loại tương tự hoặc khớp, trong đó các vị trí gọi có thể trở nên khó đọc hoặc dễ xảy ra lỗi khi ghi
- Định cấu hình việc tạo một đối tượng theo gia số, trong đó mỗi đoạn mã cấu hình khác nhau có thể gọi trên trình tạo dưới dạng chi tiết triển khai
- Cho phép một loại phát triển bằng cách thêm các tham số tạo tuỳ chọn bổ sung trong các phiên bản API trong tương lai
Nếu có một loại có từ 3 tham số bắt buộc trở xuống và không có tham số không bắt buộc, thì bạn gần như luôn có thể bỏ qua trình tạo và sử dụng một hàm khởi tạo đơn giản.
Các lớp có nguồn gốc từ Kotlin nên ưu tiên các hàm dựng được chú thích @JvmOverloads
có đối số mặc định thay vì Trình tạo, nhưng có thể chọn cải thiện khả năng sử dụng cho các ứng dụng Java bằng cách cung cấp cả Trình tạo trong các trường hợp đã nêu trước đó.
class Tone @JvmOverloads constructor(
val duration: Long = 1000,
val frequency: Int = 2600,
val dtmfConfigs: List<DtmfConfig> = emptyList()
) {
class Builder {
// ...
}
}
Các lớp trình tạo phải trả về trình tạo
Các lớp trình tạo phải cho phép tạo chuỗi phương thức bằng cách trả về đối tượng Trình tạo (chẳng hạn như this
) từ mọi phương thức, ngoại trừ build()
. Bạn nên truyền các đối tượng đã tạo bổ sung dưới dạng đối số – không trả về trình tạo của một đối tượng khác.
Ví dụ:
public static class Builder {
public void setDuration(long);
public void setFrequency(int);
public DtmfConfigBuilder addDtmfConfig();
public Tone build();
}
public class Tone {
public static class Builder {
public Builder setDuration(long);
public Builder setFrequency(int);
public Builder addDtmfConfig(DtmfConfig);
public Tone build();
}
}
Trong những trường hợp hiếm gặp khi một lớp trình tạo cơ sở phải hỗ trợ tiện ích, hãy dùng kiểu trả về chung:
public abstract class Builder<T extends Builder<T>> {
abstract T setValue(int);
}
public class TypeBuilder<T extends TypeBuilder<T>> extends Builder<T> {
T setValue(int);
T setTypeSpecificValue(long);
}
Bạn phải tạo các lớp trình tạo thông qua một hàm khởi tạo
Để duy trì việc tạo trình tạo nhất quán thông qua giao diện API Android, bạn phải tạo tất cả trình tạo thông qua một hàm khởi tạo chứ không phải phương thức trình tạo tĩnh. Đối với các API dựa trên Kotlin, Builder
phải là công khai ngay cả khi người dùng Kotlin dự kiến sẽ ngầm dựa vào trình tạo thông qua cơ chế tạo kiểu phương thức/DSL của nhà máy. Các thư viện không được dùng @PublishedApi internal
để chọn lọc ẩn hàm khởi tạo lớp Builder
khỏi các ứng dụng Kotlin.
public class Tone {
public static Builder builder();
public static class Builder {
}
}
public class Tone {
public static class Builder {
public Builder();
}
}
Tất cả đối số cho hàm khởi tạo trình tạo đều phải bắt buộc (chẳng hạn như @NonNull)
Không bắt buộc, ví dụ: @Nullable
, các đối số phải được chuyển sang phương thức setter.
Hàm khởi tạo của trình tạo sẽ gửi một NullPointerException
(hãy cân nhắc sử dụng Objects.requireNonNull
) nếu bạn không chỉ định bất kỳ đối số bắt buộc nào.
Các lớp trình tạo phải là các lớp tĩnh bên trong cuối cùng của các loại được tạo
Để tổ chức logic trong một gói, các lớp trình tạo thường phải được hiển thị dưới dạng các lớp bên trong cuối cùng của các loại được tạo, ví dụ: Tone.Builder
thay vì ToneBuilder
.
Trình tạo có thể bao gồm một hàm khởi tạo để tạo một thực thể mới từ một thực thể hiện có
Trình tạo có thể bao gồm một hàm sao chép để tạo một phiên bản trình tạo mới từ trình tạo hoặc đối tượng đã tạo hiện có. Chúng không nên cung cấp các phương thức thay thế để tạo các thực thể trình tạo từ các trình tạo hoặc đối tượng bản dựng hiện có.
public class Tone {
public static class Builder {
public Builder clone();
}
public Builder toBuilder();
}
public class Tone {
public static class Builder {
public Builder(Builder original);
public Builder(Tone original);
}
}
Các phương thức setter của trình tạo phải lấy các đối số @Nullable nếu trình tạo có hàm dựng sao chép
Việc đặt lại là cần thiết nếu một phiên bản mới của trình tạo có thể được tạo từ một phiên bản hiện có. Nếu không có hàm khởi tạo sao chép, thì trình tạo có thể có đối số @Nullable
hoặc @NonNullable
.
public static class Builder {
public Builder(Builder original);
public Builder setObjectValue(@Nullable Object value);
}
Phương thức setter của trình tạo có thể nhận các đối số @Nullable cho các thuộc tính không bắt buộc
Việc sử dụng giá trị có thể rỗng cho đầu vào cấp hai thường đơn giản hơn, đặc biệt là trong Kotlin, sử dụng các đối số mặc định thay vì trình tạo và hàm nạp chồng.
Ngoài ra, các phương thức thiết lập @Nullable
sẽ so khớp các phương thức này với phương thức truy xuất của chúng. Phương thức truy xuất phải là @Nullable
đối với các thuộc tính không bắt buộc.
Value createValue(@Nullable OptionalValue optionalValue) {
Value.Builder builder = new Value.Builder();
if (optionalValue != null) {
builder.setOptionalValue(optionalValue);
}
return builder.build();
}
Value createValue(@Nullable OptionalValue optionalValue) {
return new Value.Builder()
.setOptionalValue(optionalValue);
.build();
}
// Or in other cases:
Value createValue() {
return new Value.Builder()
.setOptionalValue(condition ? new OptionalValue() : null);
.build();
}
Cách sử dụng phổ biến trong Kotlin:
fun createValue(optionalValue: OptionalValue? = null) =
Value.Builder()
.apply { optionalValue?.let { setOptionalValue(it) } }
.build()
fun createValue(optionalValue: OptionalValue? = null) =
Value.Builder()
.setOptionalValue(optionalValue)
.build()
Giá trị mặc định (nếu phương thức thiết lập không được gọi) và ý nghĩa của null
phải được ghi lại đúng cách trong cả phương thức thiết lập và phương thức truy xuất.
/**
* ...
*
* <p>Defaults to {@code null}, which means the optional value won't be used.
*/
Bạn có thể cung cấp phương thức setter của trình tạo cho các thuộc tính có thể thay đổi, trong đó phương thức setter có sẵn trên lớp được tạo
Nếu lớp của bạn có các thuộc tính có thể thay đổi và cần có lớp Builder
, trước tiên, hãy tự hỏi xem lớp của bạn có thực sự cần có các thuộc tính có thể thay đổi hay không.
Tiếp theo, nếu bạn chắc chắn rằng mình cần các thuộc tính có thể thay đổi, hãy quyết định xem trường hợp nào trong số các trường hợp sau đây phù hợp hơn với trường hợp sử dụng dự kiến của bạn:
Đối tượng được tạo phải có thể sử dụng ngay, do đó, bạn nên cung cấp các phương thức thiết lập cho tất cả các thuộc tính có liên quan, cho dù có thể thay đổi hay không thể thay đổi.
map.put(key, new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .setUsefulMutableProperty(usefulValue) .build());
Bạn có thể cần thực hiện thêm một số lệnh gọi trước khi đối tượng được tạo có thể hữu ích, do đó, bạn không nên cung cấp các setter cho các thuộc tính có thể thay đổi.
Value v = new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .build(); v.setUsefulMutableProperty(usefulValue) Result r = v.performSomeAction(); Key k = callSomeMethod(r); map.put(k, v);
Đừng kết hợp hai trường hợp này.
Value v = new Value.Builder(requiredValue)
.setImmutableProperty(immutableValue)
.setUsefulMutableProperty(usefulValue)
.build();
Result r = v.performSomeAction();
Key k = callSomeMethod(r);
map.put(k, v);
Trình tạo không được có phương thức getter
Phương thức getter phải nằm trên đối tượng đã tạo, chứ không phải trình tạo.
Các phương thức setter của trình tạo phải có các phương thức getter tương ứng trên lớp được tạo
public class Tone {
public static class Builder {
public Builder setDuration(long);
public Builder setFrequency(int);
public Builder addDtmfConfig(DtmfConfig);
public Tone build();
}
}
public class Tone {
public static class Builder {
public Builder setDuration(long);
public Builder setFrequency(int);
public Builder addDtmfConfig(DtmfConfig);
public Tone build();
}
public long getDuration();
public int getFrequency();
public @NonNull List<DtmfConfig> getDtmfConfigs();
}
Đặt tên phương thức của trình tạo
Tên phương thức của trình tạo phải sử dụng kiểu setFoo()
, addFoo()
hoặc clearFoo()
.
Các lớp Builder dự kiến sẽ khai báo một phương thức build()
Các lớp trình tạo phải khai báo một phương thức build()
trả về một thực thể của đối tượng được tạo.
Các phương thức build() của trình tạo phải trả về các đối tượng @NonNull
Phương thức build()
của trình tạo dự kiến sẽ trả về một thực thể khác rỗng của đối tượng được tạo. Trong trường hợp không thể tạo đối tượng do các tham số không hợp lệ, bạn có thể hoãn quy trình xác thực sang phương thức tạo và IllegalStateException
sẽ được truyền.
Không cung cấp các khoá nội bộ
Các phương thức trong API công khai không được sử dụng từ khoá synchronized
. Từ khoá này khiến đối tượng hoặc lớp của bạn được dùng làm khoá. Vì khoá này được cung cấp cho những người khác, nên bạn có thể gặp phải các tác dụng phụ không mong muốn nếu mã khác bên ngoài lớp của bạn bắt đầu dùng khoá này cho mục đích khoá.
Thay vào đó, hãy thực hiện mọi thao tác khoá cần thiết đối với một đối tượng nội bộ, riêng tư.
public synchronized void doThing() { ... }
private final Object mThingLock = new Object();
public void doThing() {
synchronized (mThingLock) {
...
}
}
Các phương thức theo kiểu công cụ truy cập phải tuân thủ các nguyên tắc về thuộc tính của Kotlin
Khi xem từ các nguồn Kotlin, các phương thức theo kiểu trình truy cập (những phương thức sử dụng tiền tố get
, set
hoặc is
) cũng sẽ có sẵn dưới dạng các thuộc tính Kotlin.
Ví dụ: int getField()
được xác định trong Java có sẵn trong Kotlin dưới dạng thuộc tính val field: Int
.
Vì lý do này và để đáp ứng kỳ vọng chung của nhà phát triển về hành vi của phương thức truy cập, các phương thức sử dụng tiền tố phương thức truy cập sẽ hoạt động tương tự như các trường Java. Tránh sử dụng tiền tố theo kiểu trình truy cập khi:
- Phương thức này có tác dụng phụ – nên dùng tên phương thức mô tả rõ ràng hơn
- Phương thức này liên quan đến công việc tốn nhiều tài nguyên tính toán – nên dùng
compute
- Phương thức này liên quan đến việc chặn hoặc công việc khác diễn ra trong thời gian dài để trả về một giá trị, chẳng hạn như IPC hoặc I/O khác – nên dùng
fetch
- Phương thức này chặn luồng cho đến khi có thể trả về một giá trị – nên dùng
await
- Phương thức này trả về một phiên bản đối tượng mới trên mỗi lệnh gọi – nên dùng
create
- Phương thức này có thể không trả về giá trị thành công – nên dùng
request
Xin lưu ý rằng việc thực hiện công việc tốn nhiều tài nguyên tính toán một lần và lưu giá trị vào bộ nhớ đệm cho các lệnh gọi tiếp theo vẫn được tính là thực hiện công việc tốn nhiều tài nguyên tính toán. Hiện tượng giật không được phân bổ đều cho các khung hình.
Sử dụng tiền tố is cho các phương thức truy cập boolean
Đây là quy ước đặt tên tiêu chuẩn cho các phương thức và trường boolean trong Java. Nhìn chung, tên biến và phương thức boolean phải được viết dưới dạng các câu hỏi được trả lời bằng giá trị trả về.
Các phương thức truy cập boolean của Java phải tuân theo một lược đồ đặt tên set
/is
và các trường nên ưu tiên is
, như trong:
// Visibility is a direct property. The object "is" visible:
void setVisible(boolean visible);
boolean isVisible();
// Factory reset protection is an indirect property.
void setFactoryResetProtectionEnabled(boolean enabled);
boolean isFactoryResetProtectionEnabled();
final boolean isAvailable;
Việc sử dụng set
/is
cho các phương thức truy cập Java hoặc is
cho các trường Java sẽ cho phép sử dụng các phương thức này làm thuộc tính từ Kotlin:
obj.isVisible = true
obj.isFactoryResetProtectionEnabled = false
if (!obj.isAvailable) return
Các thuộc tính và phương thức truy cập thường nên sử dụng tên tích cực, ví dụ: Enabled
thay vì Disabled
. Việc sử dụng thuật ngữ phủ định sẽ đảo ngược ý nghĩa của true
và false
, đồng thời khiến bạn khó giải thích hành vi hơn.
// Passing false here is a double-negative.
void setFactoryResetProtectionDisabled(boolean disabled);
Trong trường hợp giá trị boolean mô tả việc bao gồm hoặc quyền sở hữu một thuộc tính, bạn có thể sử dụng has thay vì is; tuy nhiên, điều này sẽ không hoạt động với cú pháp thuộc tính Kotlin:
// Transient state is an indirect property used to track state
// related to the object. The object is not transient; rather,
// the object "has" transient state associated with it:
void setHasTransientState(boolean hasTransientState);
boolean hasTransientState();
Một số tiền tố thay thế có thể phù hợp hơn bao gồm có thể và nên:
// "Can" describes a behavior that the object may provide,
// and here is more concise than setRecordingEnabled or
// setRecordingAllowed. The object "can" record:
void setCanRecord(boolean canRecord);
boolean canRecord();
// "Should" describes a hint or property that is not strictly
// enforced, and here is more explicit than setFitWidthEnabled.
// The object "should" fit width:
void setShouldFitWidth(boolean shouldFitWidth);
boolean shouldFitWidth();
Các phương thức chuyển đổi hành vi hoặc tính năng có thể sử dụng tiền tố is và hậu tố Enabled:
// "Enabled" describes the availability of a property, and is
// more appropriate here than "can use" or "should use" the
// property:
void setWiFiRoamingSettingEnabled(boolean enabled)
boolean isWiFiRoamingSettingEnabled()
Tương tự, các phương thức cho biết sự phụ thuộc vào các hành vi hoặc tính năng khác có thể sử dụng tiền tố is và hậu tố Supported hoặc Required:
// "Supported" describes whether this API would work on devices that support
// multiple users. The API "supports" multi-user:
void setMultiUserSupported(boolean supported)
boolean isMultiUserSupported()
// "Required" describes whether this API depends on devices that support
// multiple users. The API "requires" multi-user:
void setMultiUserRequired(boolean required)
boolean isMultiUserRequired()
Thông thường, tên phương thức phải được viết dưới dạng các câu hỏi được trả lời bằng giá trị trả về.
Phương thức thuộc tính Kotlin
Đối với một thuộc tính lớp var foo: Foo
, Kotlin sẽ tạo các phương thức get
/set
bằng một quy tắc nhất quán: thêm get
vào đầu và viết hoa ký tự đầu tiên cho phương thức getter, đồng thời thêm set
vào đầu và viết hoa ký tự đầu tiên cho phương thức setter. Khai báo thuộc tính sẽ tạo ra các phương thức có tên lần lượt là public Foo getFoo()
và public void setFoo(Foo foo)
.
Nếu thuộc tính thuộc loại Boolean
, thì một quy tắc bổ sung sẽ áp dụng trong quá trình tạo tên: nếu tên thuộc tính bắt đầu bằng is
, thì get
sẽ không được thêm vào trước cho tên phương thức getter, bản thân tên thuộc tính sẽ được dùng làm getter.
Do đó, bạn nên đặt tên cho các thuộc tính Boolean
bằng tiền tố is
để tuân theo nguyên tắc đặt tên:
var isVisible: Boolean
Nếu tài sản của bạn thuộc một trong những trường hợp ngoại lệ nêu trên và bắt đầu bằng một tiền tố thích hợp, hãy sử dụng chú thích @get:JvmName
trên tài sản để chỉ định tên thích hợp theo cách thủ công:
@get:JvmName("hasTransientState")
var hasTransientState: Boolean
@get:JvmName("canRecord")
var canRecord: Boolean
@get:JvmName("shouldFitWidth")
var shouldFitWidth: Boolean
Trình truy cập mặt nạ bit
Hãy xem phần Sử dụng @IntDef
cho các cờ mặt nạ bit để biết các nguyên tắc API liên quan đến việc xác định các cờ mặt nạ bit.
Setter
Bạn nên cung cấp 2 phương thức setter: một phương thức lấy chuỗi bit đầy đủ và ghi đè tất cả các cờ hiện có, còn phương thức kia lấy một mặt nạ bit tuỳ chỉnh để cho phép linh hoạt hơn.
/**
* Sets the state of all scroll indicators.
* <p>
* See {@link #setScrollIndicators(int, int)} for usage information.
*
* @param indicators a bitmask of indicators that should be enabled, or
* {@code 0} to disable all indicators
* @see #setScrollIndicators(int, int)
* @see #getScrollIndicators()
*/
public void setScrollIndicators(@ScrollIndicators int indicators);
/**
* Sets the state of the scroll indicators specified by the mask. To change
* all scroll indicators at once, see {@link #setScrollIndicators(int)}.
* <p>
* When a scroll indicator is enabled, it will be displayed if the view
* can scroll in the direction of the indicator.
* <p>
* Multiple indicator types may be enabled or disabled by passing the
* logical OR of the specified types. If multiple types are specified, they
* will all be set to the same enabled state.
* <p>
* For example, to enable the top scroll indicator:
* {@code setScrollIndicators(SCROLL_INDICATOR_TOP, SCROLL_INDICATOR_TOP)}
* <p>
* To disable the top scroll indicator:
* {@code setScrollIndicators(0, SCROLL_INDICATOR_TOP)}
*
* @param indicators a bitmask of values to set; may be a single flag,
* the logical OR of multiple flags, or 0 to clear
* @param mask a bitmask indicating which indicator flags to modify
* @see #setScrollIndicators(int)
* @see #getScrollIndicators()
*/
public void setScrollIndicators(@ScrollIndicators int indicators, @ScrollIndicators int mask);
Phương thức getter
Bạn phải cung cấp một phương thức getter để lấy toàn bộ mặt nạ bit.
/**
* Returns a bitmask representing the enabled scroll indicators.
* <p>
* For example, if the top and left scroll indicators are enabled and all
* other indicators are disabled, the return value will be
* {@code View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_LEFT}.
* <p>
* To check whether the bottom scroll indicator is enabled, use the value
* of {@code (getScrollIndicators() & View.SCROLL_INDICATOR_BOTTOM) != 0}.
*
* @return a bitmask representing the enabled scroll indicators
*/
@ScrollIndicators
public int getScrollIndicators();
Sử dụng công khai thay vì được bảo vệ
Luôn ưu tiên public
hơn protected
trong API công khai. Quyền truy cập được bảo vệ sẽ gây khó khăn về lâu dài, vì người triển khai phải ghi đè để cung cấp các trình truy cập công khai trong trường hợp quyền truy cập bên ngoài theo mặc định cũng sẽ tốt như vậy.
Hãy nhớ rằng chế độ hiển thị protected
không ngăn nhà phát triển gọi một API – chế độ này chỉ khiến việc gọi API trở nên khó chịu hơn một chút.
Không triển khai hoặc triển khai cả equals() và hashCode()
Nếu ghi đè một phương thức, bạn phải ghi đè phương thức còn lại.
Triển khai toString() cho các lớp dữ liệu
Bạn nên ghi đè toString()
cho các lớp dữ liệu để giúp nhà phát triển gỡ lỗi mã của họ.
Ghi lại xem đầu ra có phải là cho hành vi của chương trình hay gỡ lỗi hay không
Quyết định xem bạn có muốn hành vi của chương trình phụ thuộc vào việc triển khai của bạn hay không. Ví dụ: UUID.toString() và File.toString() sẽ ghi lại định dạng cụ thể của chúng để các chương trình sử dụng. Nếu bạn chỉ hiển thị thông tin để gỡ lỗi, chẳng hạn như Intent, thì hãy ngầm hiểu rằng bạn kế thừa tài liệu từ siêu lớp.
Đừng cung cấp thêm thông tin
Tất cả thông tin có trong toString()
cũng phải có trong API công khai của đối tượng. Nếu không, bạn đang khuyến khích nhà phát triển phân tích cú pháp và dựa vào đầu ra toString()
của bạn, điều này sẽ ngăn chặn các thay đổi trong tương lai. Bạn nên triển khai toString()
chỉ bằng API công khai của đối tượng.
Không nên dựa vào kết quả gỡ lỗi
Mặc dù không thể ngăn các nhà phát triển dựa vào đầu ra gỡ lỗi, nhưng việc đưa System.identityHashCode
của đối tượng vào đầu ra toString()
sẽ khiến hai đối tượng khác nhau khó có thể có đầu ra toString()
bằng nhau.
@Override
public String toString() {
return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + " {mFoo=" + mFoo + "}";
}
Điều này có thể ngăn chặn hiệu quả việc nhà phát triển viết các câu lệnh kiểm thử như assertThat(a.toString()).isEqualTo(b.toString())
trên các đối tượng của bạn.
Sử dụng createFoo khi trả về các đối tượng mới tạo
Sử dụng tiền tố create
, không phải get
hoặc new
, cho các phương thức sẽ tạo giá trị trả về, ví dụ: bằng cách tạo các đối tượng mới.
Khi phương thức sẽ tạo một đối tượng để trả về, hãy nêu rõ điều đó trong tên phương thức.
public FooThing getFooThing() {
return new FooThing();
}
public FooThing createFooThing() {
return new FooThing();
}
Các phương thức chấp nhận đối tượng Tệp cũng phải chấp nhận các luồng
Vị trí lưu trữ dữ liệu trên Android không phải lúc nào cũng là tệp trên ổ đĩa. Ví dụ: nội dung được truyền qua các ranh giới người dùng được biểu thị dưới dạng content://
Uri
. Để cho phép xử lý nhiều nguồn dữ liệu, các API chấp nhận đối tượng File
cũng phải chấp nhận InputStream
, OutputStream
hoặc cả hai.
public void setDataSource(File file)
public void setDataSource(InputStream stream)
Lấy và trả về các kiểu nguyên thuỷ thay vì các phiên bản đóng hộp
Nếu bạn cần truyền đạt các giá trị bị thiếu hoặc giá trị rỗng, hãy cân nhắc sử dụng -1
, Integer.MAX_VALUE
hoặc Integer.MIN_VALUE
.
public java.lang.Integer getLength()
public void setLength(java.lang.Integer)
public int getLength()
public void setLength(int value)
Việc tránh các lớp tương đương của các loại dữ liệu nguyên thuỷ giúp tránh được chi phí bộ nhớ của các lớp này, quyền truy cập phương thức vào các giá trị và quan trọng hơn là việc tự động đóng hộp đến từ việc truyền giữa các loại dữ liệu nguyên thuỷ và đối tượng. Việc tránh những hành vi này giúp tiết kiệm bộ nhớ và các hoạt động phân bổ tạm thời có thể dẫn đến việc thu thập rác tốn kém và thường xuyên hơn.
Sử dụng chú giải để làm rõ tham số và giá trị trả về hợp lệ
Chúng tôi đã thêm chú thích của nhà phát triển để giúp làm rõ các giá trị được phép trong nhiều trường hợp. Điều này giúp các công cụ dễ dàng hỗ trợ nhà phát triển khi họ cung cấp các giá trị không chính xác (ví dụ: truyền một int
tuỳ ý khi khung yêu cầu một trong một tập hợp giá trị hằng số cụ thể). Hãy sử dụng bất kỳ và tất cả chú giải sau đây khi thích hợp:
Tính chất rỗng
Bạn phải dùng chú giải tính chất rỗng rõ ràng cho các API Java, nhưng khái niệm về tính chất rỗng là một phần của ngôn ngữ Kotlin và bạn không bao giờ được dùng chú giải tính chất rỗng trong các API Kotlin.
@Nullable
: Cho biết một giá trị trả về, tham số hoặc trường nhất định có thể có giá trị rỗng:
@Nullable
public String getName()
public void setName(@Nullable String name)
@NonNull
: Cho biết rằng một giá trị trả về, tham số hoặc trường nhất định không thể có giá trị rỗng. Việc đánh dấu các mục là @Nullable
tương đối mới đối với Android, vì vậy, hầu hết các phương thức API của Android đều không được ghi lại một cách nhất quán. Do đó, chúng ta có trạng thái ba chiều là "không xác định, @Nullable
, @NonNull
", đó là lý do @NonNull
nằm trong nguyên tắc về API:
@NonNull
public String getName()
public void setName(@NonNull String name)
Đối với tài liệu về nền tảng Android, việc chú thích các tham số phương thức sẽ tự động tạo tài liệu dưới dạng "Giá trị này có thể là giá trị rỗng", trừ phi "null" được dùng rõ ràng ở nơi khác trong tài liệu tham số.
Các phương thức "không thực sự có thể rỗng" hiện có: Các phương thức hiện có trong API không có chú thích @Nullable
được khai báo có thể được chú thích @Nullable
nếu phương thức có thể trả về null
trong những trường hợp cụ thể, rõ ràng (chẳng hạn như findViewById()
). Các phương thức @NotNull requireFoo()
đi kèm sẽ gửi IllegalArgumentException
cần được thêm cho những nhà phát triển không muốn kiểm tra giá trị rỗng.
Phương thức giao diện: các API mới phải thêm chú thích thích hợp khi triển khai các phương thức giao diện, chẳng hạn như Parcelable.writeToParcel()
(tức là phương thức đó trong lớp triển khai phải là writeToParcel(@NonNull Parcel,
int)
, chứ không phải writeToParcel(Parcel, int)
); tuy nhiên, các API hiện có thiếu chú thích không cần phải "cố định".
Thực thi tính chất rỗng
Trong Java, bạn nên sử dụng các phương thức để thực hiện quy trình xác thực đầu vào cho các tham số @NonNull
bằng cách sử dụng Objects.requireNonNull()
và truyền NullPointerException
khi các tham số có giá trị rỗng. Quá trình này được thực hiện tự động trong Kotlin.
Tài nguyên
Giá trị nhận dạng tài nguyên: Các tham số số nguyên biểu thị mã nhận dạng cho các tài nguyên cụ thể phải được chú thích bằng định nghĩa loại tài nguyên thích hợp.
Có một chú giải cho mọi loại tài nguyên, chẳng hạn như @StringRes
, @ColorRes
và @AnimRes
, ngoài chú giải chung @AnyRes
. Ví dụ:
public void setTitle(@StringRes int resId)
@IntDef cho các tập hợp hằng số
Hằng số đặc biệt: Các tham số String
và int
được dùng để nhận một trong số hữu hạn các giá trị có thể có được biểu thị bằng hằng số công khai phải được chú thích một cách thích hợp bằng @StringDef
hoặc @IntDef
. Các chú giải này cho phép bạn tạo một chú giải mới mà bạn có thể sử dụng, hoạt động giống như một typedef cho các tham số cho phép. Ví dụ:
/** @hide */
@IntDef(prefix = {"NAVIGATION_MODE_"}, value = {
NAVIGATION_MODE_STANDARD,
NAVIGATION_MODE_LIST,
NAVIGATION_MODE_TABS
})
@Retention(RetentionPolicy.SOURCE)
public @interface NavigationMode {}
public static final int NAVIGATION_MODE_STANDARD = 0;
public static final int NAVIGATION_MODE_LIST = 1;
public static final int NAVIGATION_MODE_TABS = 2;
@NavigationMode
public int getNavigationMode();
public void setNavigationMode(@NavigationMode int mode);
Bạn nên dùng các phương thức để kiểm tra tính hợp lệ của các tham số được chú thích và gửi một IllegalArgumentException
nếu tham số không thuộc @IntDef
@IntDef cho cờ mặt nạ bit
Chú giải cũng có thể chỉ định rằng các hằng số là cờ và có thể kết hợp với & và I:
/** @hide */
@IntDef(flag = true, prefix = { "FLAG_" }, value = {
FLAG_USE_LOGO,
FLAG_SHOW_HOME,
FLAG_HOME_AS_UP,
})
@Retention(RetentionPolicy.SOURCE)
public @interface DisplayOptions {}
@StringDef cho các tập hợp hằng số chuỗi
Ngoài ra, còn có chú giải @StringDef
, giống hệt như @IntDef
trong phần trước, nhưng dành cho các hằng số String
. Bạn có thể thêm nhiều giá trị "tiền tố" được dùng để tự động phát tài liệu cho tất cả các giá trị.
@SdkConstant cho các hằng số SDK
@SdkConstant Chú thích các trường công khai khi chúng là một trong những giá trị SdkConstant
sau: ACTIVITY_INTENT_ACTION
, BROADCAST_INTENT_ACTION
, SERVICE_ACTION
, INTENT_CATEGORY
, FEATURE
.
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_CALL = "android.intent.action.CALL";
Cung cấp khả năng tương thích về tính chất rỗng cho các chế độ ghi đè
Để đảm bảo khả năng tương thích API, khả năng có giá trị rỗng của các phương thức ghi đè phải tương thích với khả năng có giá trị rỗng hiện tại của đối tượng mẹ. Bảng sau đây thể hiện những kỳ vọng về khả năng tương thích. Nói một cách đơn giản, các giá trị ghi đè chỉ nên hạn chế bằng hoặc hạn chế hơn so với phần tử mà chúng ghi đè.
Loại | Cha mẹ | Cho con cái |
---|---|---|
Kiểu dữ liệu trả về | Chưa được chú thích | Không được chú thích hoặc không rỗng |
Kiểu dữ liệu trả về | Có thể rỗng | Có thể rỗng hoặc không rỗng |
Kiểu dữ liệu trả về | Nonnull | Nonnull |
Đối số vui nhộn | Chưa được chú thích | Không được chú thích hoặc có thể rỗng |
Đối số vui nhộn | Có thể rỗng | Có thể rỗng |
Đối số vui nhộn | Nonnull | Có thể rỗng hoặc không rỗng |
Ưu tiên các đối số không thể rỗng (chẳng hạn như @NonNull) nếu có thể
Khi các phương thức bị nạp chồng, hãy ưu tiên tất cả các đối số không rỗng.
public void startActivity(@NonNull Component component) { ... }
public void startActivity(@NonNull Component component, @NonNull Bundle options) { ... }
Quy tắc này cũng áp dụng cho các phương thức thiết lập thuộc tính bị quá tải. Đối số chính phải là nonnull và việc xoá thuộc tính phải được triển khai dưới dạng một phương thức riêng biệt. Điều này ngăn chặn các lệnh gọi "vô nghĩa" khi nhà phát triển phải đặt các tham số theo sau ngay cả khi không bắt buộc.
public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode)
public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode, boolean isLoading)
// Nonsense call to clear property
setTitleItem(null, MODE_RAW, false);
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode)
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode, boolean isLoading)
public void clearTitleItem()
Ưu tiên các loại đối tượng trả về không thể rỗng (chẳng hạn như @NonNull) cho các vùng chứa
Đối với các loại vùng chứa như Bundle
hoặc Collection
, hãy trả về một vùng chứa trống và bất biến (nếu có). Trong trường hợp bạn dùng null
để phân biệt trạng thái cung cấp của một vùng chứa, hãy cân nhắc việc cung cấp một phương thức boolean riêng biệt.
@NonNull
public Bundle getExtras() { ... }
Chú thích về khả năng có giá trị rỗng cho các cặp get và set phải giống nhau
Các cặp phương thức nhận và đặt cho một thuộc tính logic duy nhất phải luôn đồng ý trong chú thích về khả năng rỗng của chúng. Nếu không tuân theo nguyên tắc này, cú pháp thuộc tính của Kotlin sẽ không hoạt động. Do đó, việc thêm các chú thích về khả năng chấp nhận giá trị rỗng không nhất quán vào các phương thức thuộc tính hiện có là một thay đổi làm hỏng nguồn đối với người dùng Kotlin.
@NonNull
public Bundle getExtras() { ... }
public void setExtras(@NonNull Bundle bundle) { ... }
Trả về giá trị trong trường hợp thất bại hoặc lỗi
Tất cả API đều phải cho phép ứng dụng phản ứng với các lỗi. Việc trả về false
, -1
, null
hoặc các giá trị chung khác của "đã xảy ra lỗi" không cho nhà phát triển biết đủ thông tin về lỗi để đặt kỳ vọng của người dùng hoặc theo dõi chính xác độ tin cậy của ứng dụng trong thực tế. Khi thiết kế một API, hãy tưởng tượng rằng bạn đang tạo một ứng dụng. Nếu bạn gặp lỗi, API có cung cấp cho bạn đủ thông tin để trình bày lỗi đó cho người dùng hoặc phản ứng một cách thích hợp không?
- Bạn có thể (và nên) đưa thông tin chi tiết vào thông báo ngoại lệ, nhưng nhà phát triển không cần phải phân tích cú pháp thông báo đó để xử lý lỗi một cách thích hợp. Mã lỗi chi tiết hoặc thông tin khác phải được hiển thị dưới dạng các phương thức.
- Đảm bảo lựa chọn xử lý lỗi mà bạn chọn mang lại cho bạn sự linh hoạt để giới thiệu các loại lỗi mới trong tương lai. Đối với
@IntDef
, điều đó có nghĩa là bao gồm giá trịOTHER
hoặcUNKNOWN
– khi trả về mã mới, bạn có thể kiểm tratargetSdkVersion
của phương thức gọi để tránh trả về mã lỗi mà ứng dụng không biết. Đối với các trường hợp ngoại lệ, hãy có một siêu lớp chung mà các trường hợp ngoại lệ của bạn triển khai, để mọi mã xử lý loại đó cũng sẽ bắt và xử lý các kiểu phụ. - Nhà phát triển khó có thể vô tình bỏ qua lỗi – nếu lỗi của bạn được truyền đạt bằng cách trả về một giá trị, hãy chú thích phương thức của bạn bằng
@CheckResult
.
Bạn nên truyền một ? extends RuntimeException
khi đạt đến điều kiện lỗi hoặc thất bại do nhà phát triển làm sai điều gì đó, ví dụ: bỏ qua các ràng buộc đối với tham số đầu vào hoặc không kiểm tra trạng thái có thể quan sát.
Các phương thức setter hoặc phương thức hành động (ví dụ: perform
) có thể trả về một mã trạng thái số nguyên nếu hành động có thể không thành công do trạng thái hoặc điều kiện được cập nhật không đồng bộ nằm ngoài tầm kiểm soát của nhà phát triển.
Mã trạng thái phải được xác định trên lớp chứa dưới dạng các trường public static final
, có tiền tố là ERROR_
và được liệt kê trong chú thích @hide
@IntDef
.
Tên phương thức phải luôn bắt đầu bằng động từ, chứ không phải chủ ngữ
Tên của phương thức phải luôn bắt đầu bằng động từ (chẳng hạn như get
, create
, reload
, v.v.), chứ không phải đối tượng mà bạn đang thao tác.
public void tableReload() {
mTable.reload();
}
public void reloadTable() {
mTable.reload();
}
Ưu tiên các loại Collection hơn mảng làm loại tham số hoặc loại trả về
Các giao diện tập hợp được nhập chung mang lại một số lợi thế so với mảng, bao gồm cả hợp đồng API mạnh mẽ hơn về tính duy nhất và thứ tự, hỗ trợ cho các kiểu chung và một số phương thức tiện lợi thân thiện với nhà phát triển.
Ngoại lệ đối với các kiểu dữ liệu nguyên thuỷ
Nếu các phần tử là kiểu dữ liệu nguyên thuỷ, hãy ưu tiên sử dụng mảng để tránh chi phí tự động đóng gói. Xem phần Lấy và trả về các kiểu nguyên thuỷ thay vì các phiên bản đóng hộp
Trường hợp ngoại lệ đối với mã nhạy cảm về hiệu suất
Trong một số trường hợp, khi API được dùng trong mã nhạy cảm về hiệu suất (chẳng hạn như đồ hoạ hoặc các API đo lường/bố cục/vẽ khác), bạn có thể dùng mảng thay vì tập hợp để giảm số lượng phân bổ và mức sử dụng bộ nhớ.
Ngoại lệ đối với Kotlin
Mảng Kotlin là bất biến và ngôn ngữ Kotlin cung cấp nhiều API tiện ích xung quanh mảng, vì vậy, mảng ngang bằng với List
và Collection
đối với các API Kotlin được dự định truy cập từ Kotlin.
Ưu tiên các bộ sưu tập @NonNull
Luôn ưu tiên @NonNull
cho các đối tượng tập hợp. Khi trả về một tập hợp trống, hãy sử dụng phương thức Collections.empty
thích hợp để trả về một đối tượng tập hợp bất biến, được nhập chính xác và có chi phí thấp.
Khi chú thích loại được hỗ trợ, hãy luôn ưu tiên @NonNull
cho các phần tử của bộ sưu tập.
Bạn cũng nên ưu tiên @NonNull
khi sử dụng mảng thay vì bộ sưu tập (xem mục trước). Nếu bạn lo ngại về việc phân bổ đối tượng, hãy tạo một hằng số và truyền hằng số đó đi – sau cùng, một mảng trống là bất biến. Ví dụ:
private static final int[] EMPTY_USER_IDS = new int[0];
@NonNull
public int[] getUserIds() {
int [] userIds = mService.getUserIds();
return userIds != null ? userIds : EMPTY_USER_IDS;
}
Khả năng thay đổi của bộ sưu tập
Theo mặc định, các API Kotlin nên ưu tiên kiểu dữ liệu trả về chỉ đọc (không phải Mutable
) cho các tập hợp trừ phi hợp đồng API yêu cầu cụ thể kiểu dữ liệu trả về có thể thay đổi.
Tuy nhiên, theo mặc định, các API Java nên ưu tiên các kiểu trả về có thể thay đổi vì việc triển khai API Java trên nền tảng Android chưa cung cấp một cách triển khai thuận tiện cho các tập hợp bất biến. Trường hợp ngoại lệ đối với quy tắc này là các kiểu trả về Collections.empty
không thể thay đổi. Trong trường hợp tính thay đổi có thể bị các ứng dụng khai thác (cố ý hoặc vô tình) để phá vỡ mẫu sử dụng dự kiến của API, các API Java nên cân nhắc việc trả về một bản sao nông của tập hợp.
@Nullable
public PermissionInfo[] getGrantedPermissions() {
return mPermissions;
}
@NonNull
public Set<PermissionInfo> getGrantedPermissions() {
if (mPermissions == null) {
return Collections.emptySet();
}
return new ArraySet<>(mPermissions);
}
Loại dữ liệu trả về có thể thay đổi một cách rõ ràng
Tốt nhất là các API trả về các tập hợp không nên sửa đổi đối tượng tập hợp được trả về sau khi trả về. Nếu tập hợp được trả về phải thay đổi hoặc được dùng lại theo cách nào đó (ví dụ: chế độ xem được điều chỉnh của một tập dữ liệu có thể thay đổi), thì hành vi chính xác của when (khi) nội dung có thể thay đổi phải được ghi lại rõ ràng hoặc tuân theo các quy ước đặt tên API đã thiết lập.
/**
* Returns a view of this object as a list of [Item]s.
*/
fun MyObject.asList(): List<Item> = MyObjectListWrapper(this)
Quy ước .asFoo()
của Kotlin được mô tả bên dưới và cho phép bộ sưu tập do .asList()
trả về thay đổi nếu bộ sưu tập ban đầu thay đổi.
Khả năng thay đổi của các đối tượng kiểu dữ liệu được trả về
Tương tự như các API trả về các tập hợp, các API trả về các đối tượng kiểu dữ liệu lý tưởng nhất là không nên sửa đổi các thuộc tính của đối tượng được trả về sau khi trả về.
val tempResult = DataContainer()
fun add(other: DataContainer): DataContainer {
tempResult.innerValue = innerValue + other.innerValue
return tempResult
}
fun add(other: DataContainer): DataContainer {
return DataContainer(innerValue + other.innerValue)
}
Trong một số trường hợp cực kỳ hạn chế, một số mã nhạy cảm về hiệu suất có thể hưởng lợi từ việc gộp nhóm hoặc sử dụng lại đối tượng. Đừng viết cấu trúc dữ liệu nhóm đối tượng của riêng bạn và đừng hiển thị các đối tượng được dùng lại trong API công khai. Trong cả hai trường hợp, hãy hết sức cẩn thận khi quản lý quyền truy cập đồng thời.
Sử dụng loại tham số vararg
Cả API Kotlin và Java đều được khuyến khích sử dụng vararg
trong trường hợp nhà phát triển có thể tạo một mảng tại vị trí gọi chỉ nhằm mục đích truyền nhiều tham số liên quan thuộc cùng một loại.
public void setFeatures(Feature[] features) { ... }
// Developer code
setFeatures(new Feature[]{Features.A, Features.B, Features.C});
public void setFeatures(Feature... features) { ... }
// Developer code
setFeatures(Features.A, Features.B, Features.C);
Bản sao phòng vệ
Cả các phương thức triển khai Java và Kotlin của các tham số vararg
đều biên dịch thành cùng một mã byte dựa trên mảng và do đó, có thể được gọi từ mã Java bằng một mảng có thể thay đổi. Các nhà thiết kế API rất nên tạo một bản sao nông phòng thủ của tham số mảng trong trường hợp tham số đó sẽ được duy trì vào một trường hoặc lớp bên trong ẩn danh.
public void setValues(SomeObject... values) {
this.values = Arrays.copyOf(values, values.length);
}
Xin lưu ý rằng việc tạo một bản sao phòng thủ không cung cấp bất kỳ biện pháp bảo vệ nào chống lại việc sửa đổi đồng thời giữa lệnh gọi phương thức ban đầu và việc tạo bản sao, cũng như không bảo vệ chống lại việc đột biến các đối tượng có trong mảng.
Cung cấp ngữ nghĩa chính xác bằng các tham số loại bộ sưu tập hoặc các loại được trả về
List<Foo>
là lựa chọn mặc định, nhưng hãy cân nhắc các loại khác để cung cấp thêm ý nghĩa:
Sử dụng
Set<Foo>
nếu API của bạn không quan tâm đến thứ tự của các phần tử và không cho phép trùng lặp hoặc trùng lặp không có ý nghĩa.Collection<Foo>,
nếu API của bạn không quan tâm đến thứ tự và cho phép các giá trị trùng lặp.
Các hàm chuyển đổi Kotlin
Kotlin thường dùng .toFoo()
và .asFoo()
để lấy một đối tượng thuộc loại khác từ một đối tượng hiện có, trong đó Foo
là tên của loại dữ liệu trả về của quá trình chuyển đổi. Điều này nhất quán với JDK Object.toString()
quen thuộc. Kotlin tiến thêm một bước bằng cách sử dụng nó cho các lượt chuyển đổi nguyên thuỷ như 25.toFloat()
.
Sự khác biệt giữa lượt chuyển đổi có tên .toFoo()
và .asFoo()
là rất lớn:
Sử dụng .toFoo() khi tạo một đối tượng mới, độc lập
Giống như .toString()
, lượt chuyển đổi "to" sẽ trả về một đối tượng mới, độc lập. Nếu đối tượng ban đầu được sửa đổi sau đó, đối tượng mới sẽ không phản ánh những thay đổi đó.
Tương tự, nếu đối tượng mới được sửa đổi sau này, thì đối tượng cũ sẽ không phản ánh những thay đổi đó.
fun Foo.toBundle(): Bundle = Bundle().apply {
putInt(FOO_VALUE_KEY, value)
}
Sử dụng .asFoo() khi tạo một trình bao bọc phụ thuộc, đối tượng được trang trí hoặc truyền
Truyền trong Kotlin được thực hiện bằng cách dùng từ khoá as
. Thay đổi này phản ánh sự thay đổi về giao diện chứ không phải sự thay đổi về danh tính. Khi được dùng làm tiền tố trong một hàm tiện ích, .asFoo()
sẽ trang trí đối tượng nhận. Sự thay đổi trong đối tượng nhận ban đầu sẽ được phản ánh trong đối tượng do asFoo()
trả về.
Một thay đổi trong đối tượng Foo
mới có thể được phản ánh trong đối tượng ban đầu.
fun <T> Flow<T>.asLiveData(): LiveData<T> = liveData {
collect {
emit(it)
}
}
Bạn nên viết các hàm chuyển đổi dưới dạng hàm mở rộng
Việc viết các hàm chuyển đổi bên ngoài cả định nghĩa về lớp kết quả và lớp nhận sẽ giảm sự liên kết giữa các kiểu. Một lượt chuyển đổi lý tưởng chỉ cần quyền truy cập vào API công khai đối với đối tượng ban đầu. Điều này chứng minh bằng ví dụ rằng nhà phát triển cũng có thể ghi các lượt chuyển đổi tương tự vào các loại mà họ muốn.
Phát sinh các trường hợp ngoại lệ cụ thể thích hợp
Các phương thức không được gửi các trường hợp ngoại lệ chung như java.lang.Exception
hoặc java.lang.Throwable
, thay vào đó, bạn phải sử dụng một trường hợp ngoại lệ cụ thể thích hợp như java.lang.NullPointerException
để cho phép nhà phát triển xử lý các trường hợp ngoại lệ mà không quá chung chung.
Các lỗi không liên quan đến các đối số được cung cấp trực tiếp cho phương thức được gọi công khai sẽ gửi java.lang.IllegalStateException
thay vì java.lang.IllegalArgumentException
hoặc java.lang.NullPointerException
.
Trình nghe và lệnh gọi lại
Đây là các quy tắc về những lớp và phương thức được dùng cho cơ chế trình nghe và gọi lại.
Tên lớp gọi lại phải là số ít
Sử dụng MyObjectCallback
thay vì MyObjectCallbacks
.
Tên phương thức gọi lại phải có định dạng on
onFooEvent
cho biết FooEvent
đang diễn ra và lệnh gọi lại sẽ phản hồi.
Thì quá khứ và thì hiện tại phải mô tả hành vi định thời
Bạn nên đặt tên cho các phương thức gọi lại liên quan đến sự kiện để cho biết liệu sự kiện đã xảy ra hay đang diễn ra.
Ví dụ: nếu phương thức được gọi sau khi một thao tác nhấp được thực hiện:
public void onClicked()
Tuy nhiên, nếu phương thức này chịu trách nhiệm thực hiện thao tác nhấp:
public boolean onClick()
Đăng ký lệnh gọi lại
Khi có thể thêm hoặc xoá một trình nghe hoặc lệnh gọi lại khỏi một đối tượng, các phương thức được liên kết phải được đặt tên là thêm và xoá hoặc đăng ký và huỷ đăng ký. Hãy nhất quán với quy ước hiện có mà lớp hoặc các lớp khác trong cùng một gói sử dụng. Khi không có tiền lệ nào như vậy, hãy ưu tiên thêm và xoá.
Các phương thức liên quan đến việc đăng ký hoặc huỷ đăng ký lệnh gọi lại phải chỉ định toàn bộ tên của loại lệnh gọi lại.
public void addFooCallback(@NonNull FooCallback callback);
public void removeFooCallback(@NonNull FooCallback callback);
public void registerFooCallback(@NonNull FooCallback callback);
public void unregisterFooCallback(@NonNull FooCallback callback);
Tránh các phương thức getter cho lệnh gọi lại
Không thêm phương thức getFooCallback()
. Đây là một lối thoát hấp dẫn trong trường hợp nhà phát triển có thể muốn liên kết một lệnh gọi lại hiện có với lệnh gọi lại thay thế của riêng họ, nhưng nó rất dễ bị lỗi và khiến nhà phát triển thành phần khó suy luận về trạng thái hiện tại. Ví dụ:
- Nhà phát triển A gọi
setFooCallback(a)
- Nhà phát triển B gọi
setFooCallback(new B(getFooCallback()))
- Nhà phát triển A muốn xoá lệnh gọi lại
a
nhưng không có cách nào để làm như vậy mà không biết loại củaB
vàB
đã được tạo để cho phép sửa đổi lệnh gọi lại được bao bọc.
Chấp nhận Trình thực thi để kiểm soát việc gửi lệnh gọi lại
Khi đăng ký các lệnh gọi lại không có kỳ vọng rõ ràng về việc tạo luồng (hầu như ở bất kỳ đâu bên ngoài bộ công cụ giao diện người dùng), bạn nên thêm một tham số Executor
trong quá trình đăng ký để cho phép nhà phát triển chỉ định luồng mà các lệnh gọi lại sẽ được gọi.
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
Theo các nguyên tắc thông thường về tham số không bắt buộc, bạn có thể cung cấp một phương thức nạp chồng bỏ qua Executor
mặc dù đó không phải là đối số cuối cùng trong danh sách tham số. Nếu Executor
không được cung cấp, thì lệnh gọi lại sẽ được gọi trên luồng chính bằng cách sử dụng Looper.getMainLooper()
và điều này sẽ được ghi lại trên phương thức nạp chồng được liên kết.
/**
* ...
* Note that the callback will be executed on the main thread using
* {@link Looper.getMainLooper()}. To specify the execution thread, use
* {@link registerFooCallback(Executor, FooCallback)}.
* ...
*/
public void registerFooCallback(
@NonNull FooCallback callback)
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
Những điểm cần lưu ý khi triển khai Executor
: Xin lưu ý rằng sau đây là một trình thực thi hợp lệ!
public class SynchronousExecutor implements Executor {
@Override
public void execute(Runnable r) {
r.run();
}
}
Điều này có nghĩa là khi triển khai các API có dạng này, quá trình triển khai đối tượng liên kết đến trên phía quy trình ứng dụng phải gọi Binder.clearCallingIdentity()
trước khi gọi lệnh gọi lại của ứng dụng trên Executor
do ứng dụng cung cấp. Bằng cách này, mọi mã ứng dụng sử dụng danh tính liên kết (chẳng hạn như Binder.getCallingUid()
) để kiểm tra quyền đều gán chính xác mã đang chạy cho ứng dụng chứ không phải cho quy trình hệ thống gọi vào ứng dụng. Nếu người dùng API của bạn muốn có thông tin UID hoặc PID của phương thức gọi, thì đây phải là một phần rõ ràng trong giao diện API của bạn, thay vì ngầm dựa trên vị trí mà Executor
họ cung cấp đã chạy.
API của bạn phải hỗ trợ việc chỉ định một Executor
. Trong các trường hợp quan trọng về hiệu suất, các ứng dụng có thể cần chạy mã ngay lập tức hoặc đồng bộ với ý kiến phản hồi từ API của bạn. Việc chấp nhận Executor
sẽ cho phép điều này.
Việc tạo thêm một HandlerThread
hoặc tương tự như trampoline một cách phòng thủ sẽ làm mất đi trường hợp sử dụng mong muốn này.
Nếu một ứng dụng sẽ chạy mã tốn nhiều tài nguyên ở đâu đó trong quy trình của riêng ứng dụng, hãy cho phép ứng dụng đó. Các giải pháp mà nhà phát triển ứng dụng sẽ tìm ra để khắc phục các hạn chế của bạn sẽ khó hỗ trợ hơn nhiều về lâu dài.
Ngoại lệ đối với một lệnh gọi lại duy nhất: khi bản chất của các sự kiện được báo cáo chỉ hỗ trợ một thực thể lệnh gọi lại duy nhất, hãy sử dụng kiểu sau:
public void setFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
public void clearFooCallback()
Sử dụng Executor thay vì Handler
Handler
của Android từng được dùng làm tiêu chuẩn để chuyển hướng việc thực thi lệnh gọi lại đến một luồng Looper
cụ thể. Tiêu chuẩn này đã được thay đổi để ưu tiên Executor
vì hầu hết nhà phát triển ứng dụng đều quản lý nhóm luồng riêng, khiến luồng chính hoặc luồng giao diện người dùng trở thành luồng Looper
duy nhất có sẵn cho ứng dụng. Hãy sử dụng Executor
để cho phép nhà phát triển kiểm soát những gì họ cần để sử dụng lại các ngữ cảnh thực thi hiện có/ưu tiên.
Các thư viện đồng thời hiện đại như kotlinx.coroutines hoặc RxJava cung cấp cơ chế lập lịch riêng, thực hiện việc điều phối riêng khi cần. Điều này khiến việc cung cấp khả năng sử dụng trình thực thi trực tiếp (chẳng hạn như Runnable::run
) trở nên quan trọng để tránh độ trễ do hai lần chuyển luồng. Ví dụ: một bước nhảy để đăng lên một luồng Looper
bằng cách sử dụng Handler
, sau đó là một bước nhảy khác từ khung đồng thời của ứng dụng.
Nguyên tắc này hiếm khi có ngoại lệ. Sau đây là một số trường hợp kháng nghị thường gặp để xin được miễn trừ:
Tôi phải sử dụng Looper
vì tôi cần Looper
để epoll
cho sự kiện.
Yêu cầu ngoại lệ này được chấp nhận vì bạn không thể nhận được lợi ích của Executor
trong trường hợp này.
Tôi không muốn mã ứng dụng chặn luồng của tôi xuất bản sự kiện. Yêu cầu ngoại lệ này thường không được cấp cho mã chạy trong quy trình ứng dụng. Những ứng dụng làm sai điều này chỉ gây hại cho chính chúng, chứ không ảnh hưởng đến trạng thái tổng thể của hệ thống. Những ứng dụng thực hiện đúng hoặc sử dụng một khung đồng thời phổ biến sẽ không phải chịu thêm các mức phạt độ trễ.
Handler
nhất quán cục bộ với các API tương tự khác trong cùng một lớp.
Yêu cầu cấp ngoại lệ này được chấp nhận tuỳ theo trường hợp cụ thể. Bạn nên thêm các phương thức nạp chồng dựa trên Executor
, di chuyển các phương thức triển khai Handler
để sử dụng phương thức triển khai Executor
mới. (myHandler::post
là một Executor
hợp lệ!) Tuỳ thuộc vào quy mô của lớp, số lượng phương thức Handler
hiện có và khả năng nhà phát triển cần sử dụng các phương thức hiện có dựa trên Handler
cùng với phương thức mới, có thể cho phép thêm một phương thức mới dựa trên Handler
.
Tính đối xứng trong quy trình đăng ký
Nếu có cách thêm hoặc đăng ký một thứ gì đó, thì cũng phải có cách xoá/huỷ đăng ký thứ đó. Phương thức
registerThing(Thing)
phải có một
unregisterThing(Thing)
Cung cấp mã nhận dạng yêu cầu
Nếu nhà phát triển có lý do chính đáng để sử dụng lại một lệnh gọi lại, hãy cung cấp một đối tượng giá trị nhận dạng để liên kết lệnh gọi lại đó với yêu cầu.
class RequestParameters {
public int getId() { ... }
}
class RequestExecutor {
public void executeRequest(
RequestParameters parameters,
Consumer<RequestParameters> onRequestCompletedListener) { ... }
}
Đối tượng gọi lại nhiều phương thức
Các lệnh gọi lại nhiều phương thức nên ưu tiên interface
và sử dụng các phương thức default
khi thêm vào các giao diện đã phát hành trước đó. Trước đây, nguyên tắc này đề xuất abstract class
do thiếu các phương thức default
trong Java 7.
public interface MostlyOptionalCallback {
void onImportantAction();
default void onOptionalInformation() {
// Empty stub, this method is optional.
}
}
Sử dụng android.os.OutcomeReceiver khi mô hình hoá một lệnh gọi hàm không chặn
OutcomeReceiver<R,E>
báo cáo giá trị kết quả R
khi thành công hoặc E : Throwable
nếu không – những điều tương tự mà một lệnh gọi phương thức thông thường có thể làm. Sử dụng OutcomeReceiver
làm loại lệnh gọi lại khi chuyển đổi một phương thức chặn trả về kết quả hoặc gửi một ngoại lệ đến phương thức không chặn không đồng bộ:
interface FooType {
// Before:
public FooResult requestFoo(FooRequest request);
// After:
public void requestFooAsync(FooRequest request, Executor executor,
OutcomeReceiver<FooResult, Throwable> callback);
}
Các phương thức không đồng bộ được chuyển đổi theo cách này luôn trả về void
. Mọi kết quả mà requestFoo
sẽ trả về thay vào đó được báo cáo cho OutcomeReceiver.onResult
của tham số callback
của requestFooAsync
bằng cách gọi tham số đó trên executor
được cung cấp.
Mọi trường hợp ngoại lệ mà requestFoo
sẽ gửi đều được báo cáo cho phương thức OutcomeReceiver.onError
theo cách tương tự.
Việc sử dụng OutcomeReceiver
để báo cáo kết quả của phương thức không đồng bộ cũng cung cấp một trình bao bọc suspend fun
Kotlin cho các phương thức không đồng bộ bằng cách sử dụng tiện ích Continuation.asOutcomeReceiver
từ androidx.core:core-ktx
:
suspend fun FooType.requestFoo(request: FooRequest): FooResult =
suspendCancellableCoroutine { continuation ->
requestFooAsync(request, Runnable::run, continuation.asOutcomeReceiver())
}
Các tiện ích như thế này cho phép ứng dụng Kotlin gọi các phương thức không chặn không đồng bộ một cách thuận tiện bằng lệnh gọi hàm đơn giản mà không chặn luồng gọi. Những tiện ích 1-1 này cho các API nền tảng có thể được cung cấp trong cấu phần phần mềm androidx.core:core-ktx
trong Jetpack khi kết hợp với các quy trình kiểm tra và cân nhắc khả năng tương thích phiên bản tiêu chuẩn. Hãy xem tài liệu về asOutcomeReceiver để biết thêm thông tin, các điểm cần cân nhắc khi huỷ và các mẫu.
Các phương thức không đồng bộ không khớp với ngữ nghĩa của một phương thức trả về kết quả hoặc gửi một ngoại lệ khi công việc của phương thức đó hoàn tất không nên sử dụng OutcomeReceiver
làm kiểu gọi lại. Thay vào đó, hãy cân nhắc một trong những lựa chọn khác được liệt kê trong phần sau.
Ưu tiên giao diện chức năng hơn là tạo các loại phương thức trừu tượng đơn (SAM) mới
API cấp 24 đã thêm các loại java.util.function.*
(tài liệu tham khảo), cung cấp các giao diện SAM chung như Consumer<T>
phù hợp để sử dụng làm lambda gọi lại. Trong nhiều trường hợp, việc tạo các giao diện SAM mới không mang lại nhiều giá trị về độ an toàn của loại hoặc ý định giao tiếp trong khi mở rộng không cần thiết khu vực API Android.
Hãy cân nhắc sử dụng các giao diện chung này thay vì tạo giao diện mới:
Runnable
:() -> Unit
Supplier<R>
:() -> R
Consumer<T>
:(T) -> Unit
Function<T,R>
:(T) -> R
Predicate<T>
:(T) -> Boolean
- còn nhiều thông tin khác trong tài liệu tham khảo
Vị trí của các tham số SAM
Bạn nên đặt các tham số SAM ở cuối để cho phép sử dụng thành ngữ từ Kotlin, ngay cả khi phương thức đang được nạp chồng bằng các tham số bổ sung.
public void schedule(Runnable runnable)
public void schedule(int delay, Runnable runnable)
Tài liệu
Đây là các quy tắc về tài liệu công khai (Javadoc) cho API.
Bạn phải ghi lại tất cả các API công khai
Tất cả các API công khai đều phải có tài liệu đầy đủ để giải thích cách nhà phát triển sử dụng API. Giả sử nhà phát triển tìm thấy phương thức này bằng tính năng tự động hoàn thành hoặc trong khi duyệt qua tài liệu tham khảo API và có một lượng nhỏ ngữ cảnh từ giao diện API liền kề (ví dụ: cùng một lớp).
Phương thức
Các thông số phương thức và giá trị trả về phải được ghi lại bằng cách sử dụng chú thích tài liệu @param
và @return
tương ứng. Định dạng phần nội dung Javadoc như thể phần nội dung đó được đặt trước bằng "Phương thức này...".
Trong trường hợp một phương thức không nhận tham số, không có điểm đặc biệt cần lưu ý và trả về những gì tên phương thức cho biết, bạn có thể bỏ qua @return
và viết tài liệu tương tự như sau:
/**
* Returns the priority of the thread.
*/
@IntRange(from = 1, to = 10)
public int getPriority() { ... }
Luôn sử dụng đường liên kết trong Javadoc
Tài liệu cần liên kết đến các tài liệu khác để biết các hằng số, phương thức và những phần tử khác có liên quan. Sử dụng thẻ Javadoc (ví dụ: @see
và {@link foo}
), chứ không chỉ dùng các từ văn bản thuần tuý.
Đối với ví dụ về nguồn sau:
public static final int FOO = 0;
public static final int BAR = 1;
Không sử dụng văn bản thô hoặc phông chữ mã:
/**
* Sets value to one of FOO or <code>BAR</code>.
*
* @param value the value being set, one of FOO or BAR
*/
public void setValue(int value) { ... }
Thay vào đó, hãy sử dụng các đường liên kết:
/**
* Sets value to one of {@link #FOO} or {@link #BAR}.
*
* @param value the value being set
*/
public void setValue(@ValueType int value) { ... }
Xin lưu ý rằng việc sử dụng chú thích IntDef
, chẳng hạn như @ValueType
trên một tham số, sẽ tự động tạo tài liệu chỉ định các loại được phép. Hãy xem hướng dẫn về chú thích để biết thêm thông tin về IntDef
.
Chạy mục tiêu update-api hoặc docs khi thêm Javadoc
Quy tắc này đặc biệt quan trọng khi thêm thẻ @link
hoặc @see
và đảm bảo đầu ra có dạng như mong đợi. Đầu ra LỖI trong Javadoc thường là do các đường liên kết không hợp lệ. Mục tiêu update-api
hoặc docs
Make sẽ thực hiện bước kiểm tra này, nhưng mục tiêu docs
có thể nhanh hơn nếu bạn chỉ thay đổi Javadoc và không cần chạy mục tiêu update-api
.
Sử dụng {@code foo} để phân biệt các giá trị Java
Bao các giá trị Java như true
, false
và null
bằng {@code...}
để phân biệt các giá trị này với văn bản trong tài liệu.
Khi viết tài liệu trong các nguồn Kotlin, bạn có thể bao bọc mã bằng dấu nháy ngược như khi viết tài liệu bằng Markdown.
Tóm tắt @param và @return phải là một đoạn câu duy nhất
Tóm tắt tham số và giá trị trả về phải bắt đầu bằng một ký tự viết thường và chỉ chứa một đoạn câu. Nếu bạn có thêm thông tin ngoài một câu, hãy chuyển thông tin đó vào nội dung Javadoc của phương thức:
/**
* @param e The element to be appended to the list. This must not be
* null. If the list contains no entries, this element will
* be added at the beginning.
* @return This method returns true on success.
*/
Cần đổi thành:
/**
* @param e element to be appended to this list, must be non-{@code null}
* @return {@code true} on success, {@code false} otherwise
*/
Chú thích trong tài liệu cần có nội dung giải thích
Tài liệu giải thích lý do các chú thích @hide
và @removed
bị ẩn khỏi API công khai.
Đưa ra hướng dẫn về cách thay thế các phần tử API được đánh dấu bằng chú giải @deprecated
.
Sử dụng @throws để ghi lại các trường hợp ngoại lệ
Nếu một phương thức gửi một ngoại lệ đã kiểm tra, chẳng hạn như IOException
, hãy ghi lại ngoại lệ bằng @throws
. Đối với các API có nguồn gốc từ Kotlin dành cho ứng dụng Java, hãy chú thích các hàm bằng @Throws
.
Nếu một phương thức gửi ra một ngoại lệ không được kiểm tra cho biết một lỗi có thể ngăn chặn, chẳng hạn như IllegalArgumentException
hoặc IllegalStateException
, hãy ghi lại ngoại lệ kèm theo lời giải thích về lý do ngoại lệ được gửi ra. Ngoại lệ được truyền cũng phải cho biết lý do truyền.
Một số trường hợp ngoại lệ không được kiểm tra được coi là ngầm ẩn và không cần được ghi lại, chẳng hạn như NullPointerException
hoặc IllegalArgumentException
, trong đó một đối số không khớp với @IntDef
hoặc chú thích tương tự nhúng hợp đồng API vào chữ ký phương thức:
/**
* ...
* @throws IOException If it cannot find the schema for {@code toVersion}
* @throws IllegalStateException If the schema validation fails
*/
public SupportSQLiteDatabase runMigrationsAndValidate(String name, int version,
boolean validateDroppedTables, Migration... migrations) throws IOException {
// ...
if (!dbPath.exists()) {
throw new IllegalStateException("Cannot find the database file for " + name
+ ". Before calling runMigrations, you must first create the database "
+ "using createDatabase.");
}
// ...
Hoặc trong Kotlin:
/**
* ...
* @throws IOException If something goes wrong reading the file, such as a bad
* database header or missing permissions
*/
@Throws(IOException::class)
fun readVersion(databaseFile: File): Int {
// ...
val read = input.read(buffer)
if (read != 4) {
throw IOException("Bad database header, unable to read 4 bytes at " +
"offset 60")
}
}
// ...
Nếu phương thức gọi mã không đồng bộ có thể khai báo ngoại lệ, hãy cân nhắc cách nhà phát triển tìm hiểu và phản hồi những ngoại lệ đó. Thông thường, điều này liên quan đến việc chuyển tiếp ngoại lệ đến một lệnh gọi lại và ghi lại các ngoại lệ được đưa ra trên phương thức nhận chúng. Bạn không nên ghi lại các trường hợp ngoại lệ không đồng bộ bằng @throws
, trừ phi chúng thực sự được gửi lại từ phương thức được chú thích.
Kết thúc câu đầu tiên của tài liệu bằng dấu chấm
Công cụ Doclava phân tích cú pháp tài liệu một cách đơn giản, kết thúc tài liệu tóm tắt (câu đầu tiên, được dùng trong phần mô tả nhanh ở đầu tài liệu lớp) ngay khi thấy dấu chấm (.) theo sau là dấu cách. Điều này gây ra hai vấn đề:
- Nếu một tài liệu ngắn không kết thúc bằng dấu chấm và nếu thành viên đó có các tài liệu được kế thừa mà công cụ này chọn, thì bản tóm tắt cũng sẽ chọn các tài liệu được kế thừa đó. Ví dụ: hãy xem
actionBarTabStyle
trong tài liệuR.attr
. Tài liệu này có nội dung mô tả về phương diện được thêm vào bản tóm tắt. - Tránh dùng "ví dụ" trong câu đầu tiên vì cùng một lý do, vì Doclava kết thúc tài liệu tóm tắt sau "g.". Ví dụ: xem
TEXT_ALIGNMENT_CENTER
trongView.java
. Xin lưu ý rằng Metalava tự động sửa lỗi này bằng cách chèn một khoảng trắng không ngắt dòng sau dấu chấm; tuy nhiên, bạn không nên mắc phải lỗi này ngay từ đầu.
Định dạng tài liệu để hiển thị trong HTML
Javadoc được hiển thị ở định dạng HTML, vì vậy, hãy định dạng các tài liệu này cho phù hợp:
Ngắt dòng phải sử dụng thẻ
<p>
rõ ràng. Đừng thêm thẻ đóng</p>
.Không dùng ASCII để hiển thị danh sách hoặc bảng.
Danh sách nên dùng
<ul>
cho danh sách không có thứ tự và<ol>
cho danh sách có thứ tự. Mỗi mục phải bắt đầu bằng thẻ<li>
nhưng không cần thẻ đóng</li>
. Bạn phải có thẻ đóng</ul>
hoặc</ol>
sau mục cuối cùng.Bảng nên sử dụng
<table>
,<tr>
cho hàng,<th>
cho tiêu đề và<td>
cho ô. Tất cả thẻ bảng đều phải có thẻ đóng tương ứng. Bạn có thể dùngclass="deprecated"
trên thẻ bất kỳ để biểu thị việc không dùng nữa.Để tạo phông chữ mã cùng dòng, hãy sử dụng
{@code foo}
.Để tạo các khối mã, hãy sử dụng
<pre>
.Tất cả văn bản bên trong một khối
<pre>
đều được trình duyệt phân tích cú pháp, vì vậy, hãy cẩn thận với dấu ngoặc<>
. Bạn có thể thoát các ký tự này bằng thực thể HTML<
và>
.Ngoài ra, bạn có thể để lại dấu ngoặc vuông thô
<>
trong đoạn mã nếu bạn bao bọc các phần vi phạm trong{@code foo}
. Ví dụ:<pre>{@code <manifest>}</pre>
Tuân thủ hướng dẫn về phong cách tham chiếu API
Để đảm bảo tính nhất quán về kiểu cho bản tóm tắt lớp, nội dung mô tả phương thức, nội dung mô tả tham số và các mục khác, hãy làm theo các đề xuất trong nguyên tắc chính thức về ngôn ngữ Java tại Cách viết nhận xét về tài liệu cho công cụ Javadoc.
Các quy tắc dành riêng cho Khung Android
Các quy tắc này liên quan đến API, mẫu và cấu trúc dữ liệu dành riêng cho API và hành vi được tích hợp vào khung Android (ví dụ: Bundle
hoặc Parcelable
).
Trình tạo ý định nên sử dụng mẫu create*Intent()
Nhà sáng tạo cho các ý định nên sử dụng các phương thức có tên là createFooIntent()
.
Sử dụng Bundle thay vì tạo cấu trúc dữ liệu đa năng mới
Tránh tạo các cấu trúc dữ liệu đa năng mới để biểu thị các mối liên kết tuỳ ý từ khoá đến giá trị được nhập. Thay vào đó, hãy cân nhắc sử dụng Bundle
.
Điều này thường xảy ra khi viết các API nền tảng đóng vai trò là kênh giao tiếp giữa các ứng dụng và dịch vụ không thuộc nền tảng, trong đó nền tảng không đọc dữ liệu được gửi qua kênh và hợp đồng API có thể được xác định một phần bên ngoài nền tảng (ví dụ: trong một thư viện Jetpack).
Trong trường hợp nền tảng đọc dữ liệu, hãy tránh sử dụng Bundle
và ưu tiên lớp dữ liệu được nhập phần lớn.
Các hoạt động triển khai Parcelable phải có trường CREATOR công khai
Hoạt động tăng Parcelable được hiển thị thông qua CREATOR
, chứ không phải hàm khởi tạo thô. Nếu một lớp triển khai Parcelable
, thì trường CREATOR
của lớp đó cũng phải là một API công khai và hàm khởi tạo lớp nhận đối số Parcel
phải là riêng tư.
Sử dụng CharSequence cho các chuỗi giao diện người dùng
Khi một chuỗi xuất hiện trong giao diện người dùng, hãy sử dụng CharSequence
để cho phép các thực thể Spannable
.
Nếu đó chỉ là một khoá hoặc một nhãn hay giá trị nào đó mà người dùng không nhìn thấy, thì bạn có thể dùng String
.
Tránh sử dụng Enum
IntDef
phải được dùng cho các enum trong tất cả API nền tảng và nên được cân nhắc kỹ lưỡng trong các API thư viện không đi kèm. Chỉ sử dụng enum khi bạn chắc chắn rằng sẽ không có giá trị mới nào được thêm.
Lợi ích củaIntDef
:
- Cho phép thêm giá trị theo thời gian
- Các câu lệnh
when
của Kotlin có thể gặp lỗi trong thời gian chạy nếu chúng không còn đầy đủ do một giá trị enum được thêm vào nền tảng.
- Các câu lệnh
- Không có lớp hoặc đối tượng nào được dùng trong thời gian chạy, chỉ có các kiểu dữ liệu nguyên thuỷ
- Mặc dù R8 hoặc minfication có thể tránh được chi phí này cho các API thư viện không đi kèm, nhưng việc tối ưu hoá này không thể ảnh hưởng đến các lớp API nền tảng.
Lợi ích của Enum
- Tính năng ngôn ngữ đặc trưng của Java, Kotlin
- Cho phép sử dụng câu lệnh chuyển đổi toàn diện,
when
- Lưu ý: giá trị không được thay đổi theo thời gian, hãy xem danh sách trước đó
- Đặt tên rõ ràng và dễ tìm
- Cho phép xác minh thời gian biên dịch
- Ví dụ: câu lệnh
when
trong Kotlin trả về một giá trị
- Ví dụ: câu lệnh
- Là một lớp hoạt động có thể triển khai các giao diện, có các trình trợ giúp tĩnh, hiển thị các phương thức thành viên hoặc phương thức mở rộng và hiển thị các trường.
Tuân theo hệ phân cấp phân lớp gói Android
Hệ thống phân cấp gói android.*
có một thứ tự ngầm định, trong đó các gói cấp thấp hơn không thể phụ thuộc vào các gói cấp cao hơn.
Tránh đề cập đến Google, các công ty khác và sản phẩm của họ
Nền tảng Android là một dự án nguồn mở và hướng đến việc không phụ thuộc vào nhà cung cấp. API này phải là API chung và có thể được các đơn vị tích hợp hệ thống hoặc ứng dụng sử dụng như nhau với các quyền bắt buộc.
Các phương thức triển khai Parcelable phải là phương thức triển khai cuối cùng
Các lớp Parcelable do nền tảng xác định luôn được tải từ framework.jar
, vì vậy, ứng dụng không thể cố gắng ghi đè một phương thức triển khai Parcelable
.
Nếu ứng dụng gửi mở rộng một Parcelable
, thì ứng dụng nhận sẽ không có phương thức triển khai tuỳ chỉnh của người gửi để giải nén. Lưu ý về khả năng tương thích ngược: nếu lớp của bạn trước đây không phải là lớp cuối cùng nhưng không có hàm khởi tạo có sẵn công khai, bạn vẫn có thể đánh dấu lớp đó là final
.
Các phương thức gọi vào quy trình hệ thống sẽ gửi lại RemoteException dưới dạng RuntimeException
RemoteException
thường được AIDL nội bộ gửi và cho biết quy trình hệ thống đã bị tắt hoặc ứng dụng đang cố gắng gửi quá nhiều dữ liệu. Trong cả hai trường hợp, API công khai sẽ truyền lại dưới dạng RuntimeException
để ngăn các ứng dụng duy trì các quyết định về bảo mật hoặc chính sách.
Nếu bạn biết phía bên kia của lệnh gọi Binder
là quy trình hệ thống, thì mã chuẩn này là phương pháp hay nhất:
try {
...
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
Đưa ra các trường hợp ngoại lệ cụ thể cho các thay đổi về API
Hành vi của API công khai có thể thay đổi trên các cấp độ API và gây ra sự cố cho ứng dụng (ví dụ: để thực thi các chính sách bảo mật mới).
Khi API cần gửi cho một yêu cầu hợp lệ trước đó, hãy gửi một ngoại lệ cụ thể mới thay vì một ngoại lệ chung. Ví dụ: ExportedFlagRequired
thay vì SecurityException
(và ExportedFlagRequired
có thể mở rộng SecurityException
).
Việc này sẽ giúp các nhà phát triển ứng dụng và công cụ phát hiện những thay đổi về hành vi của API.
Triển khai hàm sao chép thay vì hàm sao chép
Bạn không nên sử dụng phương thức clone()
của Java do thiếu các hợp đồng API do lớp Object
cung cấp và những khó khăn vốn có trong việc mở rộng các lớp sử dụng clone()
. Thay vào đó, hãy sử dụng một hàm khởi tạo sao chép nhận một đối tượng cùng loại.
/**
* Constructs a shallow copy of {@code other}.
*/
public Foo(Foo other)
Các lớp dựa vào một Trình tạo để tạo cấu trúc nên cân nhắc việc thêm một hàm khởi tạo sao chép Trình tạo để cho phép sửa đổi bản sao.
public class Foo {
public static final class Builder {
/**
* Constructs a Foo builder using data from {@code other}.
*/
public Builder(Foo other)
Sử dụng ParcelFileDescriptor thay vì FileDescriptor
Đối tượng java.io.FileDescriptor
có định nghĩa không rõ ràng về quyền sở hữu, điều này có thể dẫn đến các lỗi sử dụng sau khi đóng không rõ ràng. Thay vào đó, các API sẽ trả về hoặc chấp nhận các thực thể ParcelFileDescriptor
. Mã cũ có thể chuyển đổi giữa PFD và FD nếu cần bằng cách sử dụng dup() hoặc getFileDescriptor().
Tránh sử dụng các giá trị số có kích thước lẻ
Tránh sử dụng trực tiếp các giá trị short
hoặc byte
, vì các giá trị này thường giới hạn khả năng phát triển API trong tương lai.
Tránh sử dụng BitSet
java.util.BitSet
rất phù hợp để triển khai nhưng không phù hợp với API công khai. Nó có thể thay đổi, yêu cầu phân bổ cho các lệnh gọi phương thức có tần suất cao và không cung cấp ý nghĩa ngữ nghĩa cho những gì mỗi bit đại diện.
Đối với các trường hợp có hiệu suất cao, hãy sử dụng int
hoặc long
với @IntDef
. Đối với các trường hợp hiệu suất thấp, hãy cân nhắc sử dụng Set<EnumType>
. Đối với dữ liệu nhị phân thô, hãy dùng byte[]
.
Ưu tiên android.net.Uri
android.net.Uri
là phương thức đóng gói ưu tiên cho URI trong API Android.
Tránh dùng java.net.URI
vì hàm này quá nghiêm ngặt trong việc phân tích cú pháp URI và không bao giờ dùng java.net.URL
vì định nghĩa về sự bình đẳng của hàm này bị hỏng nghiêm trọng.
Ẩn chú giải được đánh dấu là @IntDef, @LongDef hoặc @StringDef
Các chú thích được đánh dấu là @IntDef
, @LongDef
hoặc @StringDef
biểu thị một tập hợp các hằng số hợp lệ có thể được truyền đến một API. Tuy nhiên, khi được xuất dưới dạng chính các API, trình biên dịch sẽ nội tuyến các hằng số và chỉ các giá trị (hiện không dùng được) vẫn còn trong phần giữ chỗ API của chú thích (đối với nền tảng) hoặc JAR (đối với các thư viện).
Do đó, bạn phải đánh dấu các cách sử dụng chú thích này bằng chú thích @hide
docs trong nền tảng hoặc chú thích mã @RestrictTo.Scope.LIBRARY)
trong các thư viện. Bạn phải đánh dấu @Retention(RetentionPolicy.SOURCE)
trong cả hai trường hợp để ngăn chúng xuất hiện trong các phần giữ chỗ API hoặc JAR.
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STREAM_TYPE_FULL_IMAGE_DATA,
STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}
Khi tạo SDK nền tảng và AAR thư viện, một công cụ sẽ trích xuất các chú thích và gói riêng chúng với các nguồn đã biên dịch. Android Studio sẽ đọc định dạng đi kèm này và thực thi các định nghĩa kiểu.
Không thêm khoá nhà cung cấp chế độ cài đặt mới
Không để lộ các khoá mới từ Settings.Global
, Settings.System
hoặc Settings.Secure
.
Thay vào đó, hãy thêm một API Java getter và setter thích hợp vào một lớp có liên quan, thường là lớp "trình quản lý". Thêm cơ chế trình nghe hoặc thông báo truyền tin để thông báo cho các ứng dụng về những thay đổi khi cần.
SettingsProvider
chế độ cài đặt có một số vấn đề so với phương thức getter/setter:
- Không có tính an toàn về kiểu.
- Không có cách thống nhất để cung cấp giá trị mặc định.
- Không có cách nào phù hợp để tuỳ chỉnh quyền.
- Ví dụ: bạn không thể bảo vệ chế độ cài đặt của mình bằng một quyền tuỳ chỉnh.
- Không có cách nào phù hợp để thêm logic tuỳ chỉnh một cách thích hợp.
- Ví dụ: bạn không thể thay đổi giá trị của chế độ cài đặt A tuỳ thuộc vào giá trị của chế độ cài đặt B.
Ví dụ: Settings.Secure.LOCATION_MODE
đã tồn tại trong một thời gian dài, nhưng nhóm vị trí đã không dùng API Java thích hợp LocationManager.isLocationEnabled()
và thông báo MODE_CHANGED_ACTION
. Điều này giúp nhóm có nhiều tính linh hoạt hơn và ngữ nghĩa của các API hiện rõ ràng hơn nhiều.
Không mở rộng Activity và AsyncTask
AsyncTask
là thông tin chi tiết về cách triển khai. Thay vào đó, hãy hiển thị một trình nghe hoặc trong androidx, một API ListenableFuture
.
Không thể tạo các lớp con Activity
. Việc mở rộng hoạt động cho tính năng của bạn khiến tính năng đó không tương thích với những tính năng khác yêu cầu người dùng thực hiện thao tác tương tự. Thay vào đó, hãy dựa vào thành phần bằng cách sử dụng các công cụ như LifecycleObserver.
Sử dụng getUser() của Context
Các lớp được liên kết với một Context
, chẳng hạn như mọi thứ được trả về từ Context.getSystemService()
, nên sử dụng người dùng được liên kết với Context
thay vì hiển thị các thành viên nhắm đến những người dùng cụ thể.
class FooManager {
Context mContext;
void fooBar() {
mIFooBar.fooBarForUser(mContext.getUser());
}
}
class FooManager {
Context mContext;
Foobar getFoobar() {
// Bad: doesn't appy mContext.getUserId().
mIFooBar.fooBarForUser(Process.myUserHandle());
}
Foobar getFoobar() {
// Also bad: doesn't appy mContext.getUserId().
mIFooBar.fooBar();
}
Foobar getFoobarForUser(UserHandle user) {
mIFooBar.fooBarForUser(user);
}
}
Ngoại lệ: Một phương thức có thể chấp nhận đối số người dùng nếu chấp nhận các giá trị không đại diện cho một người dùng duy nhất, chẳng hạn như UserHandle.ALL
.
Sử dụng UserHandle thay vì các số nguyên đơn thuần
UserHandle
được ưu tiên để cung cấp sự an toàn về kiểu và tránh nhầm lẫn mã nhận dạng người dùng với uid.
Foobar getFoobarForUser(UserHandle user);
Foobar getFoobarForUser(int userId);
Nếu không thể tránh khỏi, bạn phải chú thích int
đại diện cho mã nhận dạng người dùng bằng @UserIdInt
.
Foobar getFoobarForUser(@UserIdInt int user);
Ưu tiên trình nghe hoặc lệnh gọi lại để truyền ý định
Broadcast intent rất mạnh mẽ, nhưng chúng đã dẫn đến các hành vi mới nổi có thể ảnh hưởng tiêu cực đến tình trạng của hệ thống, vì vậy, bạn nên thêm broadcast intent mới một cách thận trọng.
Sau đây là một số mối lo ngại cụ thể khiến chúng tôi không khuyến khích việc giới thiệu các broadcast intent mới:
Khi gửi thông báo truyền tin mà không có cờ
FLAG_RECEIVER_REGISTERED_ONLY
, các thông báo này sẽ buộc khởi động mọi ứng dụng chưa chạy. Mặc dù đôi khi đây là kết quả dự kiến, nhưng việc này có thể dẫn đến tình trạng hàng chục ứng dụng cùng truy cập vào một thời điểm, ảnh hưởng tiêu cực đến trạng thái của hệ thống. Bạn nên sử dụng các chiến lược thay thế, chẳng hạn nhưJobScheduler
, để phối hợp tốt hơn khi đáp ứng nhiều điều kiện tiên quyết.Khi gửi thông báo truyền tin, bạn có rất ít khả năng lọc hoặc điều chỉnh nội dung được gửi đến các ứng dụng. Điều này khiến bạn khó hoặc không thể phản hồi các mối lo ngại về quyền riêng tư trong tương lai hoặc giới thiệu các thay đổi về hành vi dựa trên SDK mục tiêu của ứng dụng nhận.
Vì hàng đợi truyền tin là một tài nguyên dùng chung, nên hàng đợi này có thể bị quá tải và có thể không dẫn đến việc phân phối sự kiện của bạn đúng hạn. Chúng tôi đã quan sát thấy một số hàng đợi truyền tin có độ trễ từ đầu đến cuối là 10 phút trở lên.
Vì những lý do này, chúng tôi khuyến khích các tính năng mới cân nhắc việc sử dụng trình nghe hoặc lệnh gọi lại hoặc các cơ sở khác như JobScheduler
thay vì ý định truyền tin.
Trong trường hợp ý định truyền tin vẫn là thiết kế lý tưởng, bạn nên cân nhắc một số phương pháp hay nhất sau đây:
- Nếu có thể, hãy dùng
Intent.FLAG_RECEIVER_REGISTERED_ONLY
để giới hạn thông báo truyền tin cho những ứng dụng đang chạy. Ví dụ:ACTION_SCREEN_ON
dùng thiết kế này để tránh đánh thức các ứng dụng. - Nếu có thể, hãy dùng
Intent.setPackage()
hoặcIntent.setComponent()
để nhắm đến chương trình phát sóng tại một ứng dụng cụ thể mà bạn quan tâm. Ví dụ:ACTION_MEDIA_BUTTON
sử dụng thiết kế này để tập trung vào ứng dụng hiện tại đang xử lý các chế độ kiểm soát chế độ phát. - Nếu có thể, hãy xác định thông báo truyền tin của bạn là
<protected-broadcast>
để ngăn các ứng dụng độc hại mạo danh hệ điều hành.
Ý định trong các dịch vụ dành cho nhà phát triển bị ràng buộc với hệ thống
Các dịch vụ mà nhà phát triển dự định mở rộng và được hệ thống liên kết, chẳng hạn như các dịch vụ trừu tượng như NotificationListenerService
, có thể phản hồi một thao tác Intent
từ hệ thống. Các dịch vụ như vậy phải đáp ứng các tiêu chí sau:
- Xác định một hằng số chuỗi
SERVICE_INTERFACE
trên lớp chứa tên lớp đủ điều kiện của dịch vụ. Hằng số này phải được chú thích bằng@SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
. - Tài liệu về lớp mà nhà phát triển phải thêm
<intent-filter>
vàoAndroidManifest.xml
của họ để nhận Intent từ nền tảng. - Hãy cân nhắc kỹ việc thêm quyền ở cấp hệ thống để ngăn các ứng dụng giả mạo gửi
Intent
đến các dịch vụ dành cho nhà phát triển.
Khả năng tương tác Kotlin-Java
Hãy xem hướng dẫn chính thức về khả năng tương tác Kotlin-Java của Android để biết danh sách đầy đủ các nguyên tắc. Một số nguyên tắc đã được sao chép vào hướng dẫn này để cải thiện khả năng khám phá.
Khả năng hiển thị của API
Một số API Kotlin, chẳng hạn như suspend fun
, không dành cho nhà phát triển Java sử dụng; tuy nhiên, đừng cố gắng kiểm soát khả năng hiển thị theo ngôn ngữ bằng cách sử dụng @JvmSynthetic
vì điều này sẽ gây ra tác dụng phụ đối với cách API được trình bày trong các trình gỡ lỗi, khiến việc gỡ lỗi trở nên khó khăn hơn.
Hãy xem hướng dẫn về khả năng tương tác giữa Kotlin và Java hoặc hướng dẫn về Async để biết hướng dẫn cụ thể.
Đối tượng companion (đồng hành)
Kotlin sử dụng companion object
để hiển thị các thành phần tĩnh. Trong một số trường hợp, các thành phần này sẽ xuất hiện từ Java trên một lớp bên trong có tên là Companion
thay vì trên lớp chứa. Các lớp Companion
có thể xuất hiện dưới dạng các lớp trống trong tệp văn bản API – đó là cách hoạt động như dự kiến.
Để tối đa hoá khả năng tương thích với Java, hãy chú thích các trường không phải hằng số của đối tượng companion bằng @JvmField
và các hàm công khai bằng @JvmStatic
để hiển thị trực tiếp các trường và hàm này trên lớp chứa.
companion object {
@JvmField val BIG_INTEGER_ONE = BigInteger.ONE
@JvmStatic fun fromPointF(pointf: PointF) {
/* ... */
}
}
Quá trình phát triển của các API nền tảng Android
Phần này mô tả các chính sách liên quan đến những loại thay đổi mà bạn có thể thực hiện đối với các API Android hiện có và cách bạn nên triển khai những thay đổi đó để tối đa hoá khả năng tương thích với các ứng dụng và cơ sở mã hiện có.
Thay đổi có thể gây lỗi nhị phân
Tránh các thay đổi có thể gây lỗi nhị phân trong các khu vực API công khai đã hoàn tất. Những loại thay đổi này thường gây ra lỗi khi chạy make update-api
, nhưng có thể có những trường hợp đặc biệt mà chế độ kiểm tra API của Metalava không phát hiện được. Khi không chắc chắn, hãy tham khảo hướng dẫn Phát triển các API dựa trên Java của Eclipse Foundation để biết giải thích chi tiết về những loại thay đổi API tương thích trong Java. Các thay đổi có thể gây lỗi nhị phân trong các API ẩn (ví dụ: hệ thống) phải tuân theo chu kỳ không dùng nữa/thay thế.
Thay đổi có thể gây lỗi
Chúng tôi không khuyến khích các thay đổi gây lỗi nguồn ngay cả khi các thay đổi đó không gây lỗi nhị phân. Một ví dụ về thay đổi tương thích nhị phân nhưng làm gián đoạn nguồn là việc thêm một thành phần chung vào một lớp hiện có, đây là tương thích nhị phân nhưng có thể gây ra lỗi biên dịch do tính kế thừa hoặc các tham chiếu không rõ ràng.
Các thay đổi làm gián đoạn nguồn sẽ không gây ra lỗi khi chạy make update-api
, vì vậy, bạn phải cẩn thận để hiểu rõ tác động của các thay đổi đối với chữ ký API hiện có.
Trong một số trường hợp, các thay đổi làm gián đoạn nguồn là cần thiết để cải thiện trải nghiệm của nhà phát triển hoặc độ chính xác của mã. Ví dụ: việc thêm chú giải về tính chất rỗng vào các nguồn Java giúp cải thiện khả năng tương tác với mã Kotlin và giảm khả năng xảy ra lỗi, nhưng thường yêu cầu thay đổi (đôi khi là thay đổi đáng kể) đối với mã nguồn.
Thay đổi đối với các API riêng tư
Bạn có thể thay đổi các API được chú thích bằng @TestApi
bất cứ lúc nào.
Bạn phải giữ lại các API được chú thích bằng @SystemApi
trong 3 năm. Bạn phải xoá hoặc tái cấu trúc một API hệ thống theo lịch trình sau:
- API y – Đã thêm
- API y+1 – Ngừng cung cấp
- Đánh dấu mã bằng
@Deprecated
. - Thêm các nội dung thay thế và liên kết đến nội dung thay thế trong Javadoc cho mã không dùng nữa bằng chú thích tài liệu
@deprecated
. - Trong chu kỳ phát triển, hãy báo cáo lỗi cho người dùng nội bộ để cho họ biết API đang bị ngừng sử dụng. Điều này giúp xác thực rằng các API thay thế là phù hợp.
- Đánh dấu mã bằng
- API y+2 – Xoá mềm
- Đánh dấu mã bằng
@removed
. - Bạn có thể chọn truyền hoặc không truyền cho các ứng dụng nhắm đến cấp SDK hiện tại cho bản phát hành.
- Đánh dấu mã bằng
- API y+3 – Xoá hoàn toàn
- Xoá hoàn toàn mã khỏi cây nguồn.
Ngừng sử dụng
Chúng tôi coi việc không dùng nữa là một thay đổi về API và có thể xảy ra trong một bản phát hành chính (chẳng hạn như chữ cái). Sử dụng chú giải nguồn @Deprecated
và chú giải tài liệu @deprecated
<summary>
cùng nhau khi không dùng nữa các API. Bản tóm tắt của bạn phải có chiến lược di chuyển. Chiến lược này có thể liên kết đến một API thay thế hoặc giải thích lý do bạn không nên sử dụng API:
/**
* Simple version of ...
*
* @deprecated Use the {@link androidx.fragment.app.DialogFragment}
* class with {@link androidx.fragment.app.FragmentManager}
* instead.
*/
@Deprecated
public final void showDialog(int id)
Bạn phải cũng không dùng các API được xác định trong XML và hiển thị trong Java, bao gồm cả các thuộc tính và thuộc tính có thể tạo kiểu được hiển thị trong lớp android.R
, kèm theo nội dung tóm tắt:
<!-- Attribute whether the accessibility service ...
{@deprecated Not used by the framework}
-->
<attr name="canRequestEnhancedWebAccessibility" format="boolean" />
Thời điểm ngừng cung cấp API
Việc ngừng sử dụng hữu ích nhất khi bạn muốn ngăn chặn việc áp dụng một API trong mã mới.
Chúng tôi cũng yêu cầu bạn đánh dấu các API là @deprecated
trước khi chúng được @removed
, nhưng điều này không tạo động lực mạnh mẽ cho các nhà phát triển di chuyển khỏi một API mà họ đang sử dụng.
Trước khi ngừng sử dụng một API, hãy cân nhắc tác động đối với nhà phát triển. Việc ngừng sử dụng API sẽ có những ảnh hưởng sau:
javac
sẽ phát ra cảnh báo trong quá trình biên dịch.- Không thể ngăn chặn hoặc đặt làm đường cơ sở các cảnh báo không dùng nữa trên toàn cầu, vì vậy, các nhà phát triển sử dụng
-Werror
cần phải sửa hoặc ngăn chặn mọi cách sử dụng API không dùng nữa trước khi có thể cập nhật phiên bản SDK biên dịch. - Bạn không thể bỏ qua cảnh báo không dùng nữa khi nhập các lớp không dùng nữa. Do đó, nhà phát triển cần phải nội tuyến tên lớp đủ điều kiện cho mọi cách sử dụng một lớp không dùng nữa trước khi có thể cập nhật phiên bản SDK biên dịch.
- Không thể ngăn chặn hoặc đặt làm đường cơ sở các cảnh báo không dùng nữa trên toàn cầu, vì vậy, các nhà phát triển sử dụng
- Tài liệu về
d.android.com
cho thấy thông báo ngừng sử dụng. - Các IDE như Android Studio sẽ hiển thị cảnh báo tại vị trí sử dụng API.
- Các IDE có thể giảm thứ hạng hoặc ẩn API khỏi tính năng tự động hoàn thành.
Do đó, việc ngừng sử dụng một API có thể khiến những nhà phát triển quan tâm nhất đến tình trạng của mã (những người sử dụng -Werror
) không muốn áp dụng các SDK mới.
Những nhà phát triển không lo ngại về cảnh báo trong mã hiện có của họ có thể hoàn toàn bỏ qua các thông báo ngừng sử dụng.
Một SDK có nhiều tính năng không dùng nữa sẽ khiến cả hai trường hợp này trở nên tồi tệ hơn.
Vì lý do này, bạn chỉ nên ngừng sử dụng API trong trường hợp:
- Chúng tôi dự định sẽ
@remove
API này trong một bản phát hành trong tương lai. - Việc sử dụng API dẫn đến hành vi không chính xác hoặc không xác định mà chúng tôi không thể khắc phục mà không làm ảnh hưởng đến khả năng tương thích.
Khi ngừng sử dụng một API và thay thế bằng một API mới, bạn nên thêm một API tương thích tương ứng vào một thư viện Jetpack như androidx.core
để đơn giản hoá việc hỗ trợ cả thiết bị cũ và mới.
Bạn không nên ngừng sử dụng các API hoạt động như dự kiến trong các bản phát hành hiện tại và trong tương lai:
/**
* ...
* @deprecated Use {@link #doThing(int, Bundle)} instead.
*/
@Deprecated
public void doThing(int action) {
...
}
public void doThing(int action, @Nullable Bundle extras) {
...
}
Việc ngừng sử dụng là phù hợp trong trường hợp API không còn duy trì được các hành vi đã ghi lại:
/**
* ...
* @deprecated No longer displayed in the status bar as of API 21.
*/
@Deprecated
public RemoteViews tickerView;
Thay đổi đối với các API không dùng nữa
Bạn phải duy trì hành vi của các API không dùng nữa. Điều này có nghĩa là các hoạt động triển khai kiểm thử phải giữ nguyên và các kiểm thử phải tiếp tục vượt qua sau khi bạn đã không dùng API nữa. Nếu API không có các kiểm thử, bạn nên thêm các kiểm thử.
Không mở rộng các nền tảng API không được dùng nữa trong các bản phát hành sau này. Bạn có thể thêm chú thích độ chính xác của lint (ví dụ: @Nullable
) vào một API không dùng nữa hiện có, nhưng không nên thêm API mới.
Không thêm API mới dưới dạng không dùng nữa. Nếu có bất kỳ API nào được thêm và sau đó không được dùng nữa trong một chu kỳ phát hành trước (do đó, ban đầu sẽ xuất hiện trên giao diện API công khai dưới dạng không được dùng nữa), thì bạn phải xoá các API đó trước khi hoàn tất API.
Xoá mềm
Xoá mềm là một thay đổi có thể gây lỗi về nguồn và bạn nên tránh thay đổi này trong các API công khai, trừ phi Hội đồng API phê duyệt rõ ràng.
Đối với các API hệ thống, bạn phải ngừng sử dụng API trong thời gian phát hành chính trước khi xoá mềm. Xoá tất cả các tài liệu tham khảo đến các API và sử dụng chú thích tài liệu @removed <summary>
khi xoá mềm các API. Phần tóm tắt của bạn phải nêu rõ lý do gỡ bỏ và có thể bao gồm một chiến lược di chuyển, như chúng tôi đã giải thích trong phần Ngừng cung cấp.
Hành vi của các API bị xoá tạm thời có thể được duy trì như cũ, nhưng quan trọng hơn là phải được giữ nguyên để các đối tượng gọi hiện tại không gặp sự cố khi gọi API. Trong một số trường hợp, điều đó có thể có nghĩa là duy trì hành vi.
Bạn phải duy trì mức độ kiểm thử, nhưng có thể cần thay đổi nội dung của các kiểm thử để phù hợp với những thay đổi về hành vi. Các kiểm thử vẫn phải xác thực rằng những đối tượng gọi hiện có không gặp sự cố trong thời gian chạy. Bạn có thể duy trì hành vi của các API bị xoá mềm như hiện tại, nhưng quan trọng hơn là bạn phải giữ nguyên hành vi đó để những đối tượng gọi hiện tại sẽ không gặp sự cố khi gọi API. Trong một số trường hợp, điều đó có thể có nghĩa là duy trì hành vi.
Bạn phải duy trì mức độ phù hợp của kiểm thử, nhưng nội dung của kiểm thử có thể cần thay đổi để phù hợp với các thay đổi về hành vi. Các kiểm thử vẫn phải xác thực rằng những đối tượng gọi hiện có không gặp sự cố trong thời gian chạy.
Ở cấp độ kỹ thuật, chúng tôi xoá API khỏi JAR gốc SDK và đường dẫn lớp thời gian biên dịch bằng chú thích Javadoc @remove
, nhưng API này vẫn tồn tại trên đường dẫn lớp thời gian chạy – tương tự như API @hide
:
/**
* Ringer volume. This is ...
*
* @removed Not functional since API 2.
*/
public static final String VOLUME_RING = ...
Theo quan điểm của nhà phát triển ứng dụng, API này sẽ không còn xuất hiện trong tính năng tự động hoàn thành và mã nguồn tham chiếu đến API sẽ không biên dịch khi compileSdk
bằng hoặc muộn hơn SDK mà API đã bị xoá; tuy nhiên, mã nguồn vẫn tiếp tục biên dịch thành công đối với các SDK và tệp nhị phân trước đó tham chiếu đến API vẫn tiếp tục hoạt động.
Bạn không được xoá mềm một số danh mục API. Bạn không được xoá mềm một số danh mục API.
Phương thức trừu tượng
Bạn không được xoá nhẹ các phương thức trừu tượng trên những lớp mà nhà phát triển có thể mở rộng. Việc này khiến nhà phát triển không thể mở rộng thành công lớp trên tất cả các cấp SDK.
Trong một số ít trường hợp mà nhà phát triển chưa từng và sẽ không thể mở rộng một lớp, bạn vẫn có thể xoá nhẹ các phương thức trừu tượng.
Xoá vĩnh viễn
Xoá cứng là một thay đổi làm gián đoạn nhị phân và không bao giờ được xảy ra trong các API công khai.
Chú thích bạn không nên dùng
Chúng tôi sử dụng chú thích @Discouraged
để cho biết rằng bạn không nên dùng một API trong hầu hết (>95%) trường hợp. API không nên dùng khác với API không dùng nữa ở chỗ có một trường hợp sử dụng quan trọng hẹp ngăn chặn việc không dùng nữa. Khi đánh dấu một API là không nên dùng, bạn phải cung cấp nội dung giải thích và một giải pháp thay thế:
@Discouraged(message = "Use of this function is discouraged because resource
reflection makes it harder to perform build
optimizations and compile-time verification of code. It
is much more efficient to retrieve resources by
identifier (such as `R.foo.bar`) than by name (such as
`getIdentifier()`)")
public int getIdentifier(String name, String defType, String defPackage) {
return mResourcesImpl.getIdentifier(name, defType, defPackage);
}
Bạn không nên thêm các API mới vì điều này không được khuyến khích.
Thay đổi về hành vi của các API hiện có
Trong một số trường hợp, bạn có thể muốn thay đổi hành vi triển khai của một API hiện có. Ví dụ: trong Android 7.0, chúng tôi đã cải thiện DropBoxManager
để truyền đạt rõ ràng khi nhà phát triển cố gắng đăng các sự kiện có kích thước quá lớn để gửi qua Binder
.
Tuy nhiên, để tránh gây ra vấn đề cho các ứng dụng hiện có, bạn nên duy trì hành vi an toàn cho các ứng dụng cũ. Trước đây, chúng tôi đã bảo vệ những thay đổi về hành vi này dựa trên ApplicationInfo.targetSdkVersion
của ứng dụng, nhưng gần đây, chúng tôi đã di chuyển để yêu cầu sử dụng Khung tương thích của ứng dụng. Sau đây là ví dụ về cách triển khai một thay đổi về hành vi bằng khung mới này:
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
public class MyClass {
@ChangeId
// This means the change will be enabled for target SDK R and higher.
@EnabledSince(targetSdkVersion=android.os.Build.VERSION_CODES.R)
// Use a bug number as the value, provide extra detail in the bug.
// FOO_NOW_DOES_X will be the change name, and 123456789 the change ID.
static final long FOO_NOW_DOES_X = 123456789L;
public void doFoo() {
if (CompatChanges.isChangeEnabled(FOO_NOW_DOES_X)) {
// do the new thing
} else {
// do the old thing
}
}
}
Việc sử dụng thiết kế Khung tương thích ứng dụng này cho phép nhà phát triển tạm thời tắt các thay đổi cụ thể về hành vi trong các bản phát hành dùng thử và beta trong quá trình gỡ lỗi ứng dụng, thay vì buộc họ phải điều chỉnh cho phù hợp với hàng chục thay đổi về hành vi cùng một lúc.
Khả năng tương thích chuyển tiếp
Khả năng tương thích chuyển tiếp là một đặc điểm thiết kế cho phép hệ thống chấp nhận dữ liệu đầu vào dành cho phiên bản sau của chính hệ thống đó. Trong trường hợp thiết kế API, bạn phải đặc biệt chú ý đến thiết kế ban đầu cũng như các thay đổi trong tương lai vì nhà phát triển mong muốn viết mã một lần, kiểm thử một lần và chạy mã ở mọi nơi mà không gặp vấn đề.
Sau đây là những nguyên nhân gây ra các vấn đề phổ biến nhất về khả năng tương thích về sau trong Android:
- Thêm hằng số mới vào một tập hợp (chẳng hạn như
@IntDef
hoặcenum
) trước đây được giả định là hoàn chỉnh (ví dụ: khiswitch
có mộtdefault
sẽ gửi một ngoại lệ). - Thêm tính năng hỗ trợ cho một tính năng không được ghi lại trực tiếp trong giao diện API (ví dụ: hỗ trợ việc chỉ định tài nguyên thuộc loại
ColorStateList
trong XML, trong khi trước đây chỉ hỗ trợ tài nguyên<color>
). - Nới lỏng các quy định hạn chế đối với quy trình kiểm tra thời gian chạy, chẳng hạn như xoá quy trình kiểm tra
requireNotNull()
có trong các phiên bản thấp hơn.
Trong tất cả những trường hợp này, nhà phát triển chỉ phát hiện ra có vấn đề khi chạy. Tệ hơn nữa, họ có thể phát hiện ra vấn đề này thông qua báo cáo sự cố từ các thiết bị cũ trên thực tế.
Ngoài ra, tất cả các trường hợp này đều là những thay đổi hợp lệ về API về mặt kỹ thuật. Chúng không làm gián đoạn khả năng tương thích nhị phân hoặc nguồn và API lint sẽ không phát hiện được bất kỳ vấn đề nào trong số này.
Do đó, các nhà thiết kế API phải hết sức chú ý khi sửa đổi các lớp hiện có. Đặt câu hỏi: "Liệu thay đổi này có khiến mã được viết và kiểm thử chỉ trên phiên bản mới nhất của nền tảng không hoạt động trên các phiên bản thấp hơn không?"
Lược đồ XML
Nếu một giản đồ XML đóng vai trò là giao diện ổn định giữa các thành phần, thì giản đồ đó phải được chỉ định rõ ràng và phải phát triển theo cách tương thích ngược, tương tự như các API Android khác. Ví dụ: cấu trúc của các phần tử và thuộc tính XML phải được giữ nguyên tương tự như cách duy trì các phương thức và biến trên các nền tảng API Android khác.
Ngừng sử dụng XML
Nếu muốn không dùng nữa một phần tử hoặc thuộc tính XML, bạn có thể thêm dấu xs:annotation
, nhưng bạn phải tiếp tục hỗ trợ mọi tệp XML hiện có bằng cách tuân theo vòng đời phát triển @SystemApi
thông thường.
<xs:element name="foo">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string">
<xs:annotation name="Deprecated"/>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
Bạn phải giữ nguyên các loại phần tử
Các giản đồ hỗ trợ phần tử sequence
, phần tử choice
và phần tử all
dưới dạng phần tử con của phần tử complexType
. Tuy nhiên, các phần tử con này khác nhau về số lượng và thứ tự của các phần tử con, vì vậy, việc sửa đổi một loại hiện có sẽ là một thay đổi không tương thích.
Nếu muốn sửa đổi một loại hiện có, bạn nên ngừng sử dụng loại cũ và giới thiệu một loại mới để thay thế.
<!-- Original "sequence" value -->
<xs:element name="foo">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string">
<xs:annotation name="Deprecated"/>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<!-- New "choice" value -->
<xs:element name="fooChoice">
<xs:complexType>
<xs:choice>
<xs:element name="name" type="xs:string"/>
</xs:choice>
</xs:complexType>
</xs:element>
Các mẫu dành riêng cho mainline
Mainline là một dự án cho phép cập nhật riêng các hệ thống con ("các mô-đun mainline") của hệ điều hành Android, thay vì cập nhật toàn bộ hình ảnh hệ thống.
Các mô-đun Mainline phải được "tách" khỏi nền tảng cốt lõi, tức là tất cả các hoạt động tương tác giữa từng mô-đun và phần còn lại của thế giới phải được thực hiện bằng cách sử dụng các API chính thức (công khai hoặc hệ thống).
Có một số mẫu thiết kế mà các mô-đun mainline phải tuân theo. Phần này mô tả các điểm khác biệt đó.
Mẫu <Module>FrameworkInitializer
Nếu một mô-đun chính cần hiển thị các lớp @SystemService
(ví dụ: JobScheduler
), hãy sử dụng mẫu sau:
Hiển thị một lớp
<YourModule>FrameworkInitializer
từ mô-đun của bạn. Lớp này cần phải nằm trong$BOOTCLASSPATH
. Ví dụ: StatsFrameworkInitializerĐánh dấu bằng
@SystemApi(client = MODULE_LIBRARIES)
.Thêm phương thức
public static void registerServiceWrappers()
vào đó.Sử dụng
SystemServiceRegistry.registerContextAwareService()
để đăng ký một lớp trình quản lý dịch vụ khi lớp đó cần tham chiếu đến mộtContext
.Sử dụng
SystemServiceRegistry.registerStaticService()
để đăng ký một lớp trình quản lý dịch vụ khi lớp đó không cần tham chiếu đến mộtContext
.Gọi phương thức
registerServiceWrappers()
từ trình khởi chạy tĩnh củaSystemServiceRegistry
.
Mẫu <Module>ServiceManager
Thông thường, để đăng ký các đối tượng liên kết dịch vụ hệ thống hoặc lấy thông tin tham chiếu đến các đối tượng đó, người ta sẽ sử dụng ServiceManager
, nhưng các mô-đun chính không thể sử dụng đối tượng này vì nó bị ẩn. Lớp này bị ẩn vì các mô-đun chính không được phép đăng ký hoặc tham chiếu đến các đối tượng liên kết dịch vụ hệ thống do nền tảng tĩnh hoặc các mô-đun khác cung cấp.
Các mô-đun chính có thể sử dụng mẫu sau đây để có thể đăng ký và nhận các tham chiếu đến các dịch vụ liên kết được triển khai bên trong mô-đun.
Tạo một lớp
<YourModule>ServiceManager
, theo thiết kế của TelephonyServiceManagerHiển thị lớp dưới dạng
@SystemApi
. Nếu chỉ cần truy cập vào lớp$BOOTCLASSPATH
hoặc lớp máy chủ hệ thống, bạn có thể sử dụng@SystemApi(client = MODULE_LIBRARIES)
; nếu không,@SystemApi(client = PRIVILEGED_APPS)
sẽ hoạt động.Lớp này sẽ bao gồm:
- Một hàm khởi tạo ẩn, vì vậy chỉ mã nền tảng tĩnh mới có thể tạo thực thể cho hàm này.
- Các phương thức getter công khai trả về một thực thể
ServiceRegisterer
cho một tên cụ thể. Nếu có một đối tượng liên kết, thì bạn cần một phương thức getter. Nếu có hai, thì bạn cần hai phương thức getter. - Trong
ActivityThread.initializeMainlineModules()
, hãy tạo thực thể cho lớp này và truyền thực thể đó đến một phương thức tĩnh do mô-đun của bạn hiển thị. Thông thường, bạn sẽ thêm một API@SystemApi(client = MODULE_LIBRARIES)
tĩnh vào lớpFrameworkInitializer
của mình.
Mẫu này sẽ ngăn các mô-đun chính khác truy cập vào những API này vì không có cách nào để các mô-đun khác nhận được một phiên bản của <YourModule>ServiceManager
, mặc dù các API get()
và register()
có thể thấy được.
Sau đây là cách điện thoại tham chiếu đến dịch vụ điện thoại: đường liên kết tìm kiếm mã.
Nếu triển khai một đối tượng liên kết dịch vụ bằng mã gốc, bạn sẽ sử dụng các API gốc AServiceManager
.
Các API này tương ứng với Java API ServiceManager
nhưng các API gốc được hiển thị trực tiếp cho các mô-đun chính. Không sử dụng các đối tượng này để đăng ký hoặc tham chiếu đến các đối tượng liên kết không thuộc sở hữu của mô-đun. Nếu bạn hiển thị một đối tượng liên kết từ gốc, thì <YourModule>ServiceManager.ServiceRegisterer
của bạn không cần phương thức register()
.
Định nghĩa về quyền trong các mô-đun chính
Các mô-đun chính chứa APK có thể xác định các quyền (tuỳ chỉnh) trong AndroidManifest.xml
APK theo cách tương tự như một APK thông thường.
Nếu quyền được xác định chỉ được dùng nội bộ trong một mô-đun, thì tên quyền của quyền đó phải có tiền tố là tên gói APK, ví dụ:
<permission
android:name="com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER"
android:protectionLevel="signature" />
Nếu quyền được xác định sẽ được cung cấp như một phần của API nền tảng có thể cập nhật cho các ứng dụng khác, thì tên quyền của quyền đó phải có tiền tố "android.permission." (giống như mọi quyền tĩnh của nền tảng) cộng với tên gói mô-đun, để báo hiệu rằng đó là một API nền tảng từ một mô-đun trong khi tránh mọi xung đột về tên, ví dụ:
<permission
android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"
android:label="@string/active_calories_burned_read_content_description"
android:protectionLevel="dangerous"
android:permissionGroup="android.permission-group.HEALTH" />
Sau đó, mô-đun có thể hiển thị tên quyền này dưới dạng hằng số API trong giao diện API, ví dụ: HealthPermissions.READ_ACTIVE_CALORIES_BURNED
.