Trang này là hướng dẫn dành cho nhà phát triển để hiểu các nguyên tắc chung mà Hội đồng API thực thi trong quy trình đánh giá 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ụ Tìm lỗi mã nguồn API. Công cụ này sẽ mã hoá nhiều quy tắc trong các bước kiểm tra để chạy theo API.
Hãy coi đây là hướng dẫn về các quy tắc mà công cụ Tìm lỗi mã nguồn tuân thủ, cùng với lời khuyên chung về các quy tắc không thể được mã hoá vào công cụ đó với độ chính xác cao.
Công cụ tìm lỗi mã nguồn API
Tìm lỗi mã nguồn API đượ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 quy trình này theo cách thủ công từ một quy trình thanh toán nền tảng cục bộ bằng m
checkapi
hoặc một quy trình thanh toán AndroidX cục bộ bằng ./gradlew :path:to:project:checkApi
.
Quy tắc 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. Các chính sách được nêu ở phần sau của 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, việc API mới 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 có thể mang lại trải nghiệm người dùng tốt hơn cho nhà phát triển ứng dụng.
Hãy dùng khả năng đánh giá của bạn và liên hệ với Hội đồng API nếu có câu hỏi khó về một API cần được giải quyết hoặc nguyên tắc cần được cập nhật.
Kiến thức 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 API Android.
Phải triển khai tất cả API
Bất kể đối tượng của API (ví dụ: công khai hoặc @SystemApi
), tất cả các nền tảng API phải được triển khai khi hợp nhất hoặc hiển thị dưới dạng API. Đừng hợp nhất các mã giả lập API với quá trình triển khai sẽ diễn ra sau này.
Các nền tảng API không triển khai có nhiều vấn đề:
- Không có gì đảm bảo rằng một nền tảng thích hợp hoặc đầy đủ đã được hiển thị. Cho đến khi ứng dụng kiểm thử hoặc sử dụng một API, sẽ không có cách nào để xác minh rằng ứng dụng đó có các API thích hợp để có thể sử dụng tính năng.
- 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 chưa được triển khai trong CTS.
Tất cả API đều phải được kiểm thử
Điều này phù hợp với các yêu cầu về CTS của nền tảng, chính sách của AndroidX và nói chung là ý tưởng về việc phải triển khai API.
Việc kiểm thử giao diện API giúp đả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. Việc kiểm thử sự tồn tại là chưa đủ; 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 chủ đề CL hoặc 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?"
Tất cả API phải được ghi lại
Tài liệu là một phần quan trọng trong khả năng hữu dụng của API. Mặc dù cú pháp của giao diện API có vẻ rõ ràng, nhưng mọi ứng dụng mới sẽ không hiểu ngữ nghĩa, hành vi hoặc ngữ cảnh đằng sau API.
Tất cả API được tạo phải tuân thủ các 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 giống như mã viết tay.
Các công cụ không nên dùng để tạo API:
AutoValue
: vi phạm nguyên tắc theo nhiều cách, ví dụ: không có cách nào để triển khai các lớp giá trị cuối cùng cũng như trình tạo cuối cùng theo cách hoạt động của AutoValue.
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 chuẩn, ngoại trừ những trường hợp được ghi chú
Các quy ước lập trình Android được ghi lại cho những người đóng góp bên ngoài tại đây:
https://source.android.com/source/code-style.html
Nhìn chung, chúng ta có xu hướng tuân theo các quy ước lập trình Java và Kotlin chuẩn.
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 sẽ tiết lộ thông tin chi tiết về cách 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à 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 hiển thị 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
có dạng như FrameLayout
cùng với các hành vi và phần tử API mới. Nếu API 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 ở trên 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ở để gửi UnsupportedOperationException
, hãy cân nhắc lại lớp cơ sở mà bạn đang sử dụng.
Sử dụng các lớp bộ sưu tập cơ sở
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 cách 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 quy tắc ràng buộc thích hợp cho API. Ví dụ: sử dụng List
cho một API có bộ sưu tập phải được sắp xếp và sử dụng Set
cho một API có bộ sưu tập phải bao gồm các phần tử duy nhất.
Trong Kotlin, hãy ưu tiên các tập hợp không thể thay đổi. Hãy xem phần Tính chất thay đổi của tập hợp để biết thêm 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ả thư viện Jetpack phải nhắm đến Java 8 trở lên.
Trong trường hợp triển khai mặc định không có trạng thái, nhà thiết kế API nên ưu tiên giao diện hơn 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 phương thức triển khai mặc định yêu cầu hàm khởi tạo hoặc trạng thái nội bộ, 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 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 nội dung mà lớp 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 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 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 kết nối nhiều lớp, hãy đặt tên có ý nghĩa cho lớp chứ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
:
- Dùng để kết hợp 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 ổn định
- Thường liên quan đến
View
Ví dụ: nếu việc điều chỉnh ngược chú giải công cụ yêu cầu duy trì trạng thái liên kết với View
và gọi một số phương thức trên View
để cài đặt bản điều chỉnh ngược, thì TooltipHelper
sẽ là tên lớp được chấp nhận.
Khô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 thông tin chi tiết về việc triển khai. Bao gồm protobuf, các ổ cắm, FlatBuffers hoặc bất kỳ nền tảng API không phải Java, không phải NDK nào khác. 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 của hướng dẫn về kiểu API (ví dụ: không thể sử dụng phương thức 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 của 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 lớp này 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ì bạn có thể tự do thay đổi giao diện này 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 bắt buộc. Ví dụ: bạn có thể cần thêm đố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 phân lô hoặc truyền trực tuyến, sử dụng bộ nhớ dùng chung hoặc 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ũng là API công khai.
Ví dụ: khô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 gói 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, 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 đề về khả năng tương thích ngược khá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 để 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 đồng nghĩa với việc 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 để phù hợ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
Đối tượng Binder
không có ý nghĩa nào và do đó không được sử dụng trong API công khai. Một trường hợp sử dụng phổ biến là sử 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 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() {...}
}
Lớp trình quản lý phải là lớp cuối cùng
Bạn nên khai báo các lớp Trình quản lý dưới dạng 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 phải tuỳ chỉnh nên hãy khai báo lớp này là final
.
Không sử dụng CompletableFuture hoặc Future
java.util.concurrent.CompletableFuture
có một giao diện API lớn cho phép
thay đổi tuỳ ý giá trị trong tương lai và có các giá trị mặc định dễ gặp lỗi
.
Ngược lại, java.util.concurrent.Future
thiếu tính năng nghe không chặn, khiến bạ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ợ 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 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 Không bắt buộc
Mặc dù Optional
có thể có lợi thế trong một số nền tảng API, nhưng không nhất quán với khu vực nền tảng API Android hiện có. @Nullable
và @NonNull
cung cấp tính năng hỗ trợ công cụ để đảm bảo an toàn cho null
và Kotlin thực thi các hợp đồng về tính chất rỗng ở cấp trình biên dịch, khiến Optional
không cần thiết.
Đối với các đối tượng gốc 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ể tạo bản sao
Các lớp chỉ có thể được tạo bằng 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ể tạo bản sao phải bao gồm ít nhất một hàm khởi tạo riêng tư để ngăn việc tạo bản sao bằng hàm khởi tạo không có đối số mặc định.
public final class Log {
// Not instantiable.
private Log() {}
}
Singleton
Bạn không nên sử dụng singleton vì chúng có các hạn chế liên quan đến kiểm thử sau:
- Lớp này quản lý việc tạo, ngăn việc sử dụng các đối tượng giả mạo
- Không thể kiểm thử kín do bản chất tĩnh của 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 một thực thể, dựa trên một lớp cơ sở trừu tượng để giải quyết các vấn đề này.
Một thực thể
Các lớp thực thể đơn 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 getInstance()
tĩnh để 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 phương thức triển khai riêng tư 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
}
}
}
Phiên bản đơn khác với singleton ở chỗ nhà phát triển có thể tạo phiên bản giả mạo của SingleInstance
và sử dụng khung Chèn 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ả mạo của riêng mình trong cấu phần phần mềm -testing
.
Các lớp giải phóng 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ự nên 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 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 ưu tiên Compose. Các tính năng giao diện người dùng mới mà nền tảng hiển thị phải được hiển thị dưới dạng API cấp thấp hơn. Các API này có thể được 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 thư viện Jetpack. Việc cung cấp các thành phần này trong thư viện sẽ tạo cơ hội để triển khai điều chỉnh cho phiên bản cũ khi các tính năng của nền tảng không có sẵn.
Trường
Các quy tắc này liên quan đến các 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 hiển thị trực tiếp các trường. Các trường phải là riêng tư và chỉ có thể truy cập bằng phương thức getter và setter 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 tăng cường hành vi chỉ định hoặc truy xuấ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.
Các trường hiển thị phải được đánh dấu là cuối cùng
Bạn không nên sử dụng các trường thô (@xem phần Không hiển thị các trường thô). Tuy nhiên, trong trường hợp hiếm hoi 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 được 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 chế độ công khai thay vì chế độ được bảo vệ
@xem Sử dụng chế độ công khai thay vì chế độ bảo vệ
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 với các giá trị int hoặc long
Cờ ngụ ý các bit có thể được kết hợp thành một số giá trị hợp nhất. Nếu không phải trường hợp nà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ờ mặt nạ bit để biết thêm thông tin về cách xác định hằng số cờ công khai.
hằng số tĩnh cuối cùng phải sử dụng quy ước đặt tên toàn chữ 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à dành cho các đối tượng 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ố chuẩn để dễ dàng xác định hơn.
Ví dụ: thông tin bổ sung về ý định phải bắt đầu bằng EXTRA_
. Hành động theo ý định phải bắt đầu bằng ACTION_
. Các hằng số được sử 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
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 phải nằm trong phạm vi 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 và không được đặt trong 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 hằng số chuỗi có giới hạn được dành riêng cho Dự án nguồn mở Android.
Bạn nên đặt tên không gian cho các thao tác và thông tin bổ sung về ý định, cũng như các mục nhập Gói bằng tên gói mà các mục đó đượ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 chế độ công khai thay vì chế độ được bảo vệ
@xem Sử dụng chế độ công khai thay vì chế độ bảo vệ
Sử dụng tiền tố nhất quán
Tất cả các hằng số có liên quan 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ố để 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
Bạn phải đặt tên cho giá trị, thuộc tính và giá trị nhận dạng công khai 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 công khai hoặc thuộc tính có chứa một tiền tố phổ biến đượ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 như
@string/config_recentsComponentName
trong config.xml - Các thuộc tính 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
Giao diện và kiểu công khai phải tuân theo quy ước đặt tên phân cấp PascalCase, 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.
Bố cục và tài nguyên có thể vẽ không được hiển thị dưới dạng API công khai. Tuy nhiên, nếu bạn phải hiển thị các thành phần này, thì bố cục và đối tượng có thể vẽ công khai phải được đặt tên bằng 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 tạo 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 lệnh gọi lại
Các hằng số được xác định trong các phiên bản API trong tương lai sẽ không được các ứng dụng nhắm đến các API cũ biết đến. Vì lý do này, các hằng số được phân phối cho ứng dụng phải xem xét phiên bản API mục tiêu của ứng dụng và liên kết các hằng số mới hơn với 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 các quy tắc ràng buộc của API cấp 22 và đưa ra giả định (hơi) hợp lý rằng chỉ có hai trạng thái có thể xảy ra. Tuy nhiên, nếu ứng dụng nhận được STATUS_FAILURE_RETRY
mới thêm, thì ứng dụng sẽ diễn giải điều nà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
trông giống như một hằng số tổng quát, thì các nhà phát triển sẽ giả định rằng các hằng số đã phát hành khi họ viết ứng dụng là đầy đủ. Nếu bạn không muốn đặt kỳ vọng này, hãy cân nhắc xem liệu hằng số tổng quát có phải là một ý 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 sẽ 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 thể mở rộng không gian tên cho các giá trị bên ngoài gói. 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ỏ các hàm tiện ích được xác định rõ ràng để tương tác với dữ liệu đó.
Đừng sử 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 API ngôn ngữ hoặc khả năng tương thích nhị phân cho mã được 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ó đố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 trình tạo khi nhắm đến ứ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 khởi tạo bản sao (Java) hoặc hàm thành viên copy()
(Kotlin) trả về một đối tượng mới.
Khi cung cấp 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à bạn phải điền các giá trị mặc định bằng cách sử dụ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
)
}
Hành vi bổ sung
Các lớp dữ liệu phải triển khai cả equals()
và hashCode()
, đồng thời phải tính đến mọi thuộc tính 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 cách 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 thông tin cụ thể trong phương thức, xung quanh các tham số, tên phương thức, loại dữ liệu trả về và chỉ định quyền truy cập.
Giờ
Các quy tắc này đề cập đến cách thể hiện các khái niệm về thời gian như ngày và thời lượng 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ó sẵn trên tất cả các phiên bản nền tảng thông qua việc đơn giản hoá 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ề.
Ưu tiên chỉ 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ể gốc có cùng trường hợp sử dụng, trừ phi miền API là miền mà việc phân bổ đối tượng theo các mẫu sử dụng dự kiến sẽ ảnh hưởng nghiêm trọng đến hiệu suất.
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 thể hiện khoảng thời gian liên quan, hãy đặt tên thông 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" (thời gian chờ) là phù hợp khi thời lượng áp dụng cụ thể cho một giá trị thời gian chờ.
"thời gian" với loại java.time.Instant
là phù hợp khi đề cập đến một điểm thời gian cụ thể, chứ không phải thời lượng.
Các phương thức biểu thị thời lượng hoặc thời gian dưới dạng dữ liệu gốc phải được đặt tên theo đơn vị thời gian và sử dụng long
Các phương thức chấp nhận hoặc trả về thời lượng dưới dạng một phương thức gốc phải thêm hậu tố vào 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
) để đặt trước tên chưa được trang trí để 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 thích hợp với đơn vị và cơ sở thời gian:
@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 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()
.
Tham số thời gian gốc hoặc giá trị trả về 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 biểu thị đơn vị thời gian nên ưu tiên viết tắt không viết tắt 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 đối số thời gian dài
Nền tảng này bao gồm một số chú thích để cung cấp tính năng nhập mạnh mẽ hơn cho các đơn vị thời gian thuộc loại long
:
@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
, do đó ở cơ sở thời gianSystem.currentTimeMillis()
.@CurrentTimeSecondsLong
: Giá trị là 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 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 biểu thị đơn vị đo lường khác với thời gian, hãy ưu tiên sử dụng 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 phương thức nạp chồng
Nếu bạn có phương thức nạp chồng với cá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 phương thức nạp chồng cho đối số không bắt buộc, hành vi của các phương thức đơn giản hơn sẽ hoạt động 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.
Kết quả: Đừng nạp chồng phương thức ngoài việc thêm đối số không bắt buộc hoặc chấp nhận nhiều loại đối số nếu phương thức đó là đa hình. Nếu phương thức nạp chồng thực hiện một việc gì đó khác về cơ bản, hãy đặt tên mới cho phương thức đó.
Các phương thức có tham số mặc định phải được chú thích bằng @JvmOverloads (chỉ dành cho 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 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 chi tiết.
class Greeting @JvmOverloads constructor(
loudness: Int = 5
) {
@JvmOverloads
fun sayHello(prefix: String = "Dr.", name: String) = // ...
}
Không xoá giá trị tham số mặc định (chỉ dành cho Kotlin)
Nếu một phương thức đã được vận chuyển cùng với một tham số có giá trị mặc định, thì việc xoá giá trị mặc định sẽ là một thay đổi gây lỗi nguồn.
Các tham số phương thức nhận dạng và khác biệt nhất nên được đặt trước tiên
Nếu bạn có một phương thức có nhiều tham số, hãy đặt những tham số có liên quan nhất lên trước. Các thông số chỉ định cờ và các tuỳ chọn khác ít quan trọng hơn các thông số mô tả đối tượng đang được thực hiện hành độ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 cùng.
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
Trình tạo
Bạn nên sử dụng mẫu Builder để tạo các đối tượng Java phức tạp và thường dùng mẫu này trong Android trong các trường hợp:
- Các thuộc tính của đối tượng thu được phải không thể thay đổi
- Có một số lượng lớn thuộc tính bắt buộc, ví dụ: nhiều đối số hàm khởi tạo
- Có một 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 biết các vấn đề về khả năng hữu dụng của API.
Cân nhắc xem bạn có cần một trình tạo hay không. Trình tạo sẽ hữu ích trong giao diện API nếu được dùng để:
- Chỉ định cấu hình một số 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 không bắt buộc hoặc bắt buộc, đôi khi là các loại tương tự hoặc khớp nhau, trong đó các trang web 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 tăng dần, trong đó một số đoạn mã cấu hình khác nhau có thể thực hiện các lệnh gọi trên trình tạo dưới dạng thông tin chi tiết về việc 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ố tuỳ chọn, thì bạn hầu như luôn có thể bỏ qua trình tạo và sử dụng hàm khởi tạo thuần tuý.
Các lớp lấy nguồn từ Kotlin nên ưu tiên các hàm khởi tạo được chú thích @JvmOverloads
với đối số mặc định hơn là Trình tạo, nhưng có thể chọn cải thiện khả năng hữu dụng cho ứng dụng Java bằng cách cũng cung cấp Trình tạo trong các trường hợp nêu trên.
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 bật tính năng 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()
. Các đối tượng tạo sẵn bổ sung phải được truyền 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 một số ít trường hợp, lớp trình tạo cơ sở phải hỗ trợ tiện ích, hãy sử dụng loại dữ liệ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 Builder thông qua 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, tất cả trình tạo phải được tạo thông qua hàm khởi tạo chứ không phải phương thức 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 phương thức nhà máy/kiểu DSL. Thư viện không được sử dụng @PublishedApi internal
để ẩn có chọn lọc hàm khởi tạo lớp Builder
khỏi ứ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 của 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 trình tạo sẽ gửi một NullPointerException
(hãy cân nhắc sử dụng Objects.requireNonNull
) nếu không chỉ định bất kỳ đối số bắt buộc nào.
Lớp trình tạo phải là các lớp bên trong tĩnh cuối cùng của các loại được tạo
Để tổ chức hợp lý trong một gói, các lớp trình tạo thường đượ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 khởi tạo sao chép để tạo một thực thể trình tạo mới từ một trình tạo hiện có hoặc đối tượng đã tạo. Các lớp này không được cung cấp các phương thức thay thế để tạo 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);
}
}
Phương thức setter của trình tạo phải lấy đối số @Nullable nếu trình tạo có hàm khởi tạo sao chép
Bạn cần phải đặt lại nếu có thể tạo một thực thể mới của một trình tạo từ một thực thể 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 Builder có thể nhận đối số @Nullable cho các thuộc tính không bắt buộc
Thường thì bạn nên sử dụng giá trị rỗng cho dữ liệu đầu vào bậc hai, đặc biệt là trong Kotlin, sử dụng các đối số mặc định thay vì trình tạo và nạp chồng.
Ngoài ra, phương thức setter @Nullable
sẽ so khớp với phương thức getter, phương thức này 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 setter 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 setter và getter.
/**
* ...
*
* <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 đã 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 liệu lớp của bạn có thực sự 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 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 lập tức, do đó, bạn nên cung cấp phương thức setter cho tất cả thuộc tính liên quan, cho dù thuộc tính đó 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 một số lệnh gọi bổ sung trước khi đối tượng được tạo có thể hữu ích, do đó, không nên cung cấp phương thứ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ên trình tạo.
Phương thức setter của Builder phải có phương thức getter tương ứng trên lớp đã 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 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.
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ể trì hoãn việc xác thực cho phương thức bản dựng và sẽ gửi một IllegalStateException
.
Không hiển thị 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à vì từ khoá này được hiển thị cho người khác, nên bạn có thể gặp phải các hiệu ứ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 sử dụng từ khoá này cho mục đích khoá.
Thay vào đó, hãy thực hiện mọi thao tác khoá bắt buộc đối với một đối tượng riêng tư, nội bộ.
public synchronized void doThing() { ... }
private final Object mThingLock = new Object();
public void doThing() {
synchronized (mThingLock) {
...
}
}
Các phương thức kiểu phương thức truy cập phải tuân thủ nguyên tắc về thuộc tính Kotlin
Khi xem từ các nguồn Kotlin, các phương thức kiểu 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 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 phải hoạt động tương tự như các trường Java. Tránh sử dụng tiền tố kiểu phương thức truy cập khi:
- Phương thức này có tác dụng phụ – nên chọn 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 kém về mặt tính toán – ưu tiên
compute
- Phương thức này liên quan đến việc chặn hoặc công việc chạy trong thời gian dài để trả về một giá trị, chẳng hạn như IPC hoặc I/O khác – ưu tiên
fetch
- Phương thức này chặn luồng cho đến khi có thể trả về một giá trị – ưu tiên
await
- Phương thức này trả về một thực thể đối tượng mới trong mỗi lệnh gọi – ưu tiên
create
- Phương thức này có thể không trả về giá trị thành công – ưu tiên
request
Xin lưu ý rằng việc thực hiện một lần công việc tốn kém về mặt tính toá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 kém về mặt tính toán. Hiện tượng giật không được phân bổ trên 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 chuẩn cho các phương thức và trường boolean trong Java. Nhìn chung, tên phương thức và biến boolean phải được viết dưới dạng câu hỏi được trả lời bằng giá trị trả về.
Phương thức truy cập boolean của Java phải tuân theo 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/trường 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 cách đặt 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ó lý giải hành vi hơn.
// Passing false here is a double-negative.
void setFactoryResetProtectionDisabled(boolean disabled);
Trong trường hợp boolean mô tả việc đưa vào hoặc quyền sở hữu của một thuộc tính, bạn có thể sử dụng has thay vì is; tuy nhiên, cách 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 can và should:
// "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 bật/tắt hành vi hoặc tính năng có thể sử dụng tiền tố is và hậu tố Enabled (Đã bật):
// "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 phần 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 (Được hỗ trợ) hoặc Required (Bắt buộc):
// "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()
Nhìn chung, tên phương thức phải được viết dưới dạng 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 thuộc tính lớp var foo: Foo
, Kotlin sẽ tạo các phương thức get
/set
bằng cách sử dụ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. Nội dung 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 tên phương thức getter, chính tên thuộc tính sẽ được dùng làm phương thức getter.
Do đó, nên đặt tên cho các thuộc tính Boolean
bằng tiền tố is
để tuân thủ nguyên tắc đặt tên:
var isVisible: Boolean
Nếu thuộc tính của bạn là một trong các 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 thuộc tính đó để 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
Phương thức truy cập mặt nạ bit
Hãy xem phần Sử dụng @IntDef
cho cờ mặt nạ bit để biết các nguyên tắc về API liên quan đến việc xác định cờ mặt nạ bit.
Phương thức setter
Bạn nên cung cấp hai phương thức setter: một phương thức lấy một chuỗi bit đầy đủ và ghi đè tất cả cờ hiện có, còn một phương thức khác 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 chế độ công khai thay vì chế độ đượ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 phiền toái về lâu dài, vì người triển khai phải ghi đè để cung cấp 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 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 API – chỉ là làm cho API đó hơi khó chịu hơn.
Không triển khai hoặc triển khai cả equals() và hashCode()
Nếu ghi đè một, bạn phải ghi đè cả hai.
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ã.
Ghi lại liệu đầu ra là cho hành vi của chương trình hay gỡ lỗi
Quyết định xem bạn có muốn hành vi của chương trình dựa vào cách triển khai của mình hay không. Ví dụ: UUID.toString() và File.toString() ghi lại định dạng cụ thể của các phương thức này để 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ư Ý định, thì hãy ngụ ý kế thừa tài liệu từ lớp cha.
Không cung cấp thông tin bổ sung
Tất cả thông tin có trong toString()
cũng phải có sẵn thông qua API công khai của đối tượng. Nếu không, bạn đang khuyến khích các nhà phát triển phân tích cú pháp và dựa vào đầu ra toString()
, đ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 nhà phát triển phụ thuộc 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 nhà phát triển viết các câu nhận định kiểm thử như assertThat(a.toString()).isEqualTo(b.toString())
trên các đối tượng của bạn một cách hiệu quả.
Sử dụng createFoo khi trả về các đối tượng mới tạo
Sử dụng tiền tố create
, chứ không phải get
hoặc new
, cho các phương thức sẽ tạo giá trị trả về, chẳng hạn như bằng cách tạo đối tượng mới.
Khi phương thức sẽ tạo một đối tượng để trả về, hãy làm 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 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 ranh giới người dùng được biểu thị dưới dạng content://
Uri
. Để bật tính năng 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 dữ liệu gốc thay vì các phiên bản đóng hộp
Nếu bạn cần thông báo về 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 với các loại dữ liệu gốc giúp tránh được hao tổn 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à tính năng tự động đóng hộp từ việc truyền giữa các loại dữ liệu gốc 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 lượt 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ố hợp lệ và giá trị trả về
Thêm chú thích dành cho nhà phát triển để giúp làm rõ các giá trị được phép trong nhiều tình huống. Điều này giúp các công cụ dễ dàng hỗ trợ nhà phát triển hơn khi họ cung cấp 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ể). Sử dụng mọi chú giải sau đây khi thích hợp:
Tính chất rỗng
Bạn phải sử dụng chú thích rõ ràng về tính chất 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 sử dụng chú thích về 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 một giá trị trả về, tham số hoặc trường nhất định không được có giá trị rỗng. Việc đánh dấu các mục là @Nullable
là 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 nhận một cách nhất quán. Do đó, chúng ta có trạng thái ba trạng thái là "không xác định, @Nullable
, @NonNull
", đó là lý do @NonNull
là một phần của nguyên tắc 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ạng "Giá trị này có thể là rỗng", trừ phi "rỗng" được sử 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ể nhận giá trị rỗng" hiện có: Các phương thức hiện có trong API không có chú thích @Nullable
đã khai báo có thể được chú thích @Nullable
nếu phương thức có thể trả về null
trong các trường hợp cụ thể, rõ ràng (chẳng hạn như findViewById()
). Bạn nên thêm các phương thức @NotNull requireFoo()
đồng hành gửi IllegalArgumentException
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: 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 "sửa".
Thực thi tính chất rỗng
Trong Java, bạn nên sử dụng các phương thức để xác thực dữ liệu đầu vào cho các tham số @NonNull
bằng cách sử dụng Objects.requireNonNull()
và gửi một NullPointerException
khi các tham số này có giá trị rỗng. Việc này được thực hiện tự động trong Kotlin.
Tài nguyên
Mã 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ú thích cho mỗi loại tài nguyên, chẳng hạn như @StringRes
, @ColorRes
và @AnimRes
, ngoài @AnyRes
tổng hợp. Ví dụ:
public void setTitle(@StringRes int resId)
@IntDef cho các tập hợp hằng số
Hằng số ma thuật: Các tham số String
và int
dùng để nhận một trong số 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 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 như một typedef cho các tham số được 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 sử 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ể được 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 hằng số String
. Bạn có thể đưa nhiều giá trị "prefix" vào để tự động phát hành 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 các trường đó là một trong các 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 tính chất rỗng tương thích cho các lệnh ghi đè
Để đảm bảo khả năng tương thích với API, tính chất rỗng của các giá trị ghi đè phải tương thích với tính chất rỗng hiện tại của phần tử mẹ. Bảng sau đây thể hiện các yêu cầu về khả năng tương thích. Rõ ràng là các phần ghi đè chỉ nên có tính hạn chế hoặc hạn chế hơn phần tử mà chúng ghi đè.
Loại | Cha mẹ | Cho con cái |
---|---|---|
Loại dữ liệu trả về | Chưa chú thích | Không chú thích hoặc không rỗng |
Kiểu dữ liệu trả về | Có thể rỗng | Có thể nhận giá trị rỗng hoặc không thể nhận giá trị rỗng |
Loại dữ liệu trả về | Nonnull | Nonnull |
Đối số thú vị | Chưa chú thích | Không chú thích hoặc có thể nhận giá trị rỗng |
Đối số thú vị | Có thể rỗng | Có thể rỗng |
Đối số thú vị | Nonnull | Có thể nhận giá trị rỗng hoặc không thể nhận giá trị rỗng |
Ưu tiên các đối số không rỗng (chẳng hạn như @NonNull) nếu có thể
Khi phương thức bị nạp chồng, hãy ưu tiên tất 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 phương thức setter của thuộc tính bị nạp chồng. Đối số chính phải là giá trị khác rỗng 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", trong đó nhà phát triển phải đặt các tham số theo sau mặc dù 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 dữ liệu trả về không thể nhận giá trị rỗng (chẳng hạn như @NonNull) cho 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à không thể thay đổi (nếu có). Trong trường hợp null
được dùng để phân biệt tình trạng có sẵn của một vùng chứa, hãy cân nhắc cung cấp một phương thức boolean riêng.
@NonNull
public Bundle getExtras() { ... }
Chú thích về tính chất rỗng cho các cặp get và set phải đồng ý
Các cặp phương thức lấy và đặt cho một thuộc tính logic phải luôn đồng ý trong chú thích tính chất rỗng. Việc không tuân thủ nguyên tắc này sẽ làm hỏng cú pháp thuộc tính của Kotlin và việc thêm chú thích tính chất rỗng không đồng ý vào các phương thức thuộc tính hiện có sẽ là một thay đổi phá vỡ nguồn cho người dùng Kotlin.
@NonNull
public Bundle getExtras() { ... }
public void setExtras(@NonNull Bundle bundle) { ... }
Trả về giá trị trong các điều kiện lỗi hoặc không thành công
Tất cả API đều phải cho phép ứng dụng phản ứng với lỗi. Việc trả về false
, -1
, null
hoặc các giá trị tổng quát khác của "đã xảy ra lỗi" không cho nhà phát triển biết đầy đủ về việc không thiết lập được 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ế API, hãy tưởng tượng bạn đang xây dựng 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 nhất thiết 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 phương thức.
- Đảm bảo rằng tuỳ chọn xử lý lỗi mà bạn chọn cho phép bạn 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 ngoại lệ, hãy có một lớp cao cấp chung mà các ngoại lệ của bạn triển khai, để mọi mã xử lý loại đó cũng sẽ phát hiện và xử lý các loại con. - Nhà phát triển khó có thể vô tình bỏ qua lỗi – nếu lỗi được thông báo 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
.
Ưu tiên gửi ? extends RuntimeException
khi gặp lỗi hoặc điều kiện lỗi do nhà phát triển làm sai, ví dụ: bỏ qua các quy tắc ràng buộc đối với tham số đầu vào hoặc không kiểm tra được trạng thái có thể quan sát.
Phương thức setter hoặ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.
Bạn nên xác định mã trạng thái 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ủ thể
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 thực hiện.
public void tableReload() {
mTable.reload();
}
public void reloadTable() {
mTable.reload();
}
Ưu tiên các loại Bộ sưu tập thay vì mảng làm loại tham số hoặc loại trả về
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ả các hợp đồng API mạnh hơn về tính duy nhất và thứ tự, hỗ trợ cho các phương thức chung và một số phương thức tiện lợi thân thiện với nhà phát triển.
Trường hợp ngoại lệ đối với dữ liệu gốc
Nếu các phần tử là dữ liệu gốc, 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 dữ liệu gốc thay vì 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 nhất định, khi API được sử 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ể sử dụng mảng thay vì bộ sưu tập để giảm mức phân bổ và mức sử dụng bộ nhớ.
Trường hợp ngoại lệ cho Kotlin
Mảng Kotlin không thay đổi và ngôn ngữ Kotlin cung cấp nhiều API tiện ích xung quanh các mảng, vì vậy, các mảng ngang bằng với List
và Collection
cho các API Kotlin dự định được 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 có chi phí thấp, được nhập chính xác và không thể thay đổi.
Khi chú thích loại được hỗ trợ, hãy luôn ưu tiên @NonNull
cho các phần tử
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ố đó – sau cùng, một mảng trống là không thể thay đổi. 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 biến đổi của bộ sưu tập
Theo mặc định, các API Kotlin nên ưu tiên các loại dữ liệu trả về chỉ có thể đọc (không phải Mutable
) cho các bộ sưu tập trừ phi hợp đồng API yêu cầu cụ thể một loại 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 loại dữ liệ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 cách triển khai thuận tiện cho các bộ sưu tập không thể thay đổi. Trường hợp ngoại lệ đối với quy tắc này là các loại dữ liệu trả về Collections.empty
không thể thay đổi. Trong trường hợp khách hàng có thể khai thác tính chất thay đổi (có chủ ý hoặc do nhầm lẫn) để 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ề tập hợp không được 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 sử dụng lại theo một cách nào đó (ví dụ: chế độ xem được điều chỉnh của tập dữ liệu có thể thay đổi), thì hành vi chính xác của thời điểm nội dung có thể thay đổi phải được ghi lại rõ ràng hoặc tuân theo 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ả dưới đây và cho phép thay đổi tập hợp do .asList()
trả về nếu tập hợp ban đầu thay đổi.
Khả năng thay đổi của các đối tượng thuộc loại dữ liệu được trả về
Tương tự như các API trả về tập hợp, các API trả về đối tượng loại dữ liệu lý tưởng nhất là không 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 rất 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 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 sử dụng lại trong API công khai. Trong cả hai trường hợp, hãy cực kỳ cẩn thận khi quản lý quyền truy cập đồng thời.
Sử dụng loại tham số vararg
Bạn nên sử dụng vararg
cho cả API Kotlin và Java trong trường hợp nhà phát triển có thể tạo một mảng tại vị trí gọi với mục đích duy nhất là truyền nhiều tham số có 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ách triển khai tham số vararg
bằng Java và Kotlin đều biên dịch thành cùng một mã byte được hỗ trợ bởi mảng, do đó, có thể được gọi từ mã Java bằng một mảng có thể thay đổi. Nhà thiết kế API nên tạo một bản sao nông bảo vệ của tham số mảng trong trường hợp tham số đó sẽ được lưu vào một trường hoặc lớp nội bộ ẩ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ệ khỏ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 tham số loại tập hợp hoặc loại dữ liệu trả về
List<Foo>
là tuỳ 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 trùng lặp.
Hàm chuyển đổi Kotlin
Kotlin thường sử 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 lượt chuyển đổi. Điều này nhất quán với JDK Object.toString()
quen thuộc. Kotlin đẩy mạnh việc này bằng cách sử dụng nó cho các lượt chuyển đổi nguyên gốc như 25.toFloat()
.
Có sự khác biệt đáng kể giữa các lượt chuyển đổi có tên là .toFoo()
và .asFoo()
:
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 gốc được sửa đổi sau này, đố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 trình bao bọc phụ thuộc, đối tượng được trang trí hoặc truyền
Việc truyền dữ liệu trong Kotlin được thực hiện bằng từ khoá as
. Thay đổi này phản ánh sự thay đổi về giao diện nhưng không phản ánh sự thay đổi về danh tính. Khi được dùng làm tiền tố trong hàm mở rộng, .asFoo()
sẽ trang trí trình nhận. Một đột biến trong đối tượng trình nhận ban đầu sẽ được phản ánh trong đối tượng do asFoo()
trả về.
Một đột biến 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 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 của trình nhận và lớp kết quả sẽ làm giảm độ liên kết giữa các loại. Một lượt chuyển đổi lý tưởng chỉ cần quyền truy cập API công khai vào đố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ể viết các lượt chuyển đổi tương tự cho các loại họ ưu tiên.
Đưa ra các 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á rộng.
Các lỗi không liên quan đến đố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 liên quan đến các lớp và phương thức dùng cho trình nghe và cơ chế gọi lại.
Tên lớp gọi lại phải ở dạng 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 trên
onFooEvent
cho biết FooEvent
đang diễn ra và lệnh gọi lại sẽ phản hồi.
Thì quá khứ so với thì hiện tại phải mô tả hành vi về thời gian
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 sự kiện đã xảy ra hay đang diễn ra.
Ví dụ: nếu phương thức được gọi sau khi thực hiện một thao tác nhấp:
public void onClicked()
Tuy nhiên, nếu phương thức chịu trách nhiệm thực hiện hành động nhấp:
public boolean onClick()
Đăng ký lệnh gọi lại
Khi có thể thêm hoặc xoá trình nghe hoặc lệnh gọi lại khỏi một đối tượng, các phương thức liên kết phải được đặt tên là thêm và xoá hoặc đăng ký và huỷ đăng ký. Phải 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 gói sử dụng. Khi không có tiền lệ 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 sử dụng phương thức getter cho lệnh gọi lại
Không thêm các 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 muốn tạo chuỗi lệnh gọi lại hiện có cùng với lệnh gọi thay thế của riêng họ, nhưng cách này không linh hoạt và khiến nhà phát triển thành phần khó lý giải 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
và không có cách nào để làm như vậy nếu không biết loại củaB
vàB
đã được tạo để cho phép các sửa đổi như vậy đối với lệnh gọi lại được gói.
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 (ở bất kỳ nơi nào bên ngoài bộ công cụ giao diện người dùng), bạn nên đưa tham số Executor
vào quy trình đăng ký để cho phép nhà phát triển chỉ định luồng mà lệnh gọi lại sẽ được gọi.
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
Ngoại lệ đối với nguyên tắc thông thường về tham số không bắt buộc, bạn có thể cung cấp 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 không cung cấp Executor
, bạn nên gọi lệnh gọi lại trên luồng chính bằng Looper.getMainLooper()
và điều này nên được ghi lại trên phương thức nạp chồng liên quan.
/**
* ...
* 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)
Các lỗi 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, việc 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 mã nhận dạng liên kết (chẳng hạn như Binder.getCallingUid()
) để kiểm tra quyền sẽ phân bổ 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 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 của giao diện API, thay vì ngầm ẩn dựa trên vị trí Executor
mà họ cung cấp đã chạy.
API của bạn phải hỗ trợ việc chỉ định Executor
. Trong các trường hợp quan trọng về hiệu suất, ứng dụng có thể cần chạy mã ngay lập tức hoặc đồng bộ với phản hồi từ API. Việc chấp nhận Executor
sẽ cho phép việc này.
Việc tạo thêm một HandlerThread
hoặc tương tự như trampoline để 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 kém ở đâu đó trong quy trình của riêng ứng dụng, hãy cho phép. Các giải pháp mà nhà phát triển ứng dụng tìm thấy để 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 lệnh gọi lại đơn: khi bản chất của các sự kiện được báo cáo chỉ yêu cầu hỗ trợ một thực thể lệnh gọi lại, hãy sử dụng kiểu sau:
public void setFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
public void clearFooCallback()
Sử dụng Trình thực thi thay vì Trình xử lý
Trước đây, Handler
của Android được dùng làm tiêu chuẩn để chuyển hướng quá trình 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 các nhà phát triển ứng dụng đều quản lý các nhóm luồng của riêng họ, 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
để cung cấp cho nhà phát triển quyền kiểm soát cần thiết để sử dụng lại các ngữ cảnh thực thi hiện có/ưu tiên của họ.
Các thư viện đồng thời hiện đại như kotlinx.coroutines hoặc RxJava cung cấp cơ chế lên lịch riêng để thực hiện việc điều phối riêng khi cần. Điều này rất quan trọng để 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
) nhằm tránh độ trễ từ việc chuyển đổi luồng kép. Ví dụ: một bước nhảy để đăng lên luồng Looper
bằng cách sử dụng Handler
, theo 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ố lý do phổ biến để yêu cầu trường hợp ngoại lệ:
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 cấp vì 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 phát hành 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 mắc lỗi này chỉ tự làm hại mình chứ không ảnh hưởng đến trạng thái tổng thể của hệ thống. Các ứng dụng sử dụng đúng cách hoặc sử dụng khung đồng thời phổ biến sẽ không phải trả thêm các hình phạt về độ 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 ngoại lệ này được cấp tuỳ theo trường hợp. Ưu tiê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 kích thước 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 dựa trên Handler
hiện có cùng với phương thức mới, một trường hợp ngoại lệ có thể được cấp để thêm một phương thức dựa trên Handler
mới.
Đối xứng trong quá trình đăng ký
Nếu có cách để thêm hoặc đăng ký một nội dung nào đó, thì cũng phải có cách để xoá/huỷ đăng ký nội dung đó. Phương thức
registerThing(Thing)
phải có một
unregisterThing(Thing)
Cung cấp giá trị nhận dạng yêu cầu
Nếu nhà phát triển có thể sử dụng lại lệnh gọi lại một cách hợp lý, 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
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 lập mô hình 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 – giống như những gì lệnh gọi phương thức thuần tuý 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 một phương thức không đồng bộ không chặn:
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
trả về sẽ đượ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 sẽ đượ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 đồng bộ không chặn bằng lệnh gọi hàm thuần tuý mà không chặn luồng gọi. Các tiện ích 1:1 này cho 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 biện pháp kiểm tra và cân nhắc về khả năng tương thích của 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ân nhắc về việc 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 được sử dụng OutcomeReceiver
làm loại lệnh gọi lại. Thay vào đó, hãy cân nhắc một trong các tuỳ 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 giao diện SAM mới mang lại ít giá trị về mặt an toàn kiểu hoặc truyền đạt ý định trong khi mở rộng không cần thiết khu vực giao diện 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ó 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 cùng để cho phép sử dụng theo cách thông thường từ Kotlin, ngay cả khi phương thức đang bị quá tải với 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.
Tất cả API công khai phải được ghi lại
Tất cả API công khai phải có đủ tài liệu để 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 ngữ cảnh tối thiểu từ giao diện API liền kề (ví dụ: cùng một lớp).
Phương thức
Bạn phải ghi lại các tham số phương thức và giá trị trả về tương ứng bằng chú thích tài liệu @param
và @return
. Định dạng phần nội dung Javadoc như thể trước đó là "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ần cân nhắc đặc biệt và trả về nội dung như 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ư:
/**
* 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 phải liên kết đến các tài liệu khác về các hằng số, phương thức và các phần tử khác có liên quan. Sử dụng thẻ Javadoc (ví dụ: @see
và {@link foo}
), chứ không chỉ các từ văn bản thuần tuý.
Đối với ví dụ 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 đườ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
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 kết quả trông như mong đợi. Kết quả ERROR trong Javadoc thường là do các đường liên kết không hợp lệ. Mục tiêu Tạo update-api
hoặc docs
sẽ thực hiện việ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
Gói 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 tài liệu.
Khi viết tài liệu trong nguồn Kotlin, bạn có thể gói mã bằng dấu nháy ngược như khi viết cho Markdown.
Tóm tắt @param và @return phải là một mảnh 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 mảnh câu duy nhất. Nếu bạn có thêm thông tin vượt quá một câu, hãy chuyển thông tin đó vào phần 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 thay đổ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
Ghi lại lý do các chú thích @hide
và @removed
bị ẩn khỏi API công khai.
Cung cấp hướng dẫn về cách thay thế các phần tử API được đánh dấu bằng chú thích @deprecated
.
Sử dụng @throws để ghi lại các ngoại lệ
Nếu một phương thức gửi một ngoại lệ đã kiểm tra, ví dụ: IOException
, hãy ghi lại ngoại lệ đó bằng @throws
. Đối với các API nguồn 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 một ngoại lệ chưa đánh dấu cho biế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 giải thích lý do ngoại lệ được gửi. Ngoại lệ được gửi cũng phải cho biết lý do ngoại lệ đó được gửi.
Một số trường hợp ngoại lệ không được đánh dấu đượ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ể gửi ngoại lệ, hãy cân nhắc cách nhà phát triển phát hiện và phản hồi các ngoại lệ đó. Thông thường, việc 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 gửi trên phương thức nhận các ngoại lệ đó. Không nên ghi lại các ngoại lệ không đồng bộ bằng @throws
, trừ phi các ngoại lệ đó 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 phần đó có các tài liệu kế thừa được công cụ chọn, thì bản tóm tắt cũng sẽ chọn các tài liệu kế thừa đó. Ví dụ: hãy xem
actionBarTabStyle
trong tài liệu vềR.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 sử dụng "ví dụ:" trong câu đầu tiên vì cùng lý do, vì Doclava kết thúc tài liệu tóm tắt sau "g.". Ví dụ: hãy xem
TEXT_ALIGNMENT_CENTER
trongView.java
. Xin lưu ý rằng Metalava sẽ tự động sửa lỗi này bằng cách chèn một dấu cách không ngắt dòng sau dấu chấm; tuy nhiên, bạn không nên mắc lỗi này ngay từ đầu.
Định dạng tài liệu để hiển thị bằng 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:
Bạn nên sử dụng thẻ
<p>
rõ ràng để ngắt dòng. Đừng thêm thẻ</p>
đóng.Không sử dụng ASCII để hiển thị danh sách hoặc bảng.
Danh sách phải sử dụng
<ul>
hoặc<ol>
tương ứng cho danh sách không theo thứ tự và danh sách theo 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ẻ</ul>
hoặc</ol>
đóng sau mục cuối cùng.Bảng phải sử dụng
<table>
,<tr>
cho hàng,<th>
cho tiêu đề và<td>
cho ô. Tất cả thẻ bảng đều cần có thẻ đóng trùng khớp. Bạn có thể sử dụngclass="deprecated"
trên bất kỳ thẻ nào để biểu thị việc ngừng sử dụng.Để tạo phông chữ mã cùng dòng, hãy sử dụng
{@code foo}
.Để tạo khối mã, hãy sử dụng
<pre>
.Tất cả văn bản bên trong 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ự đó bằng các thực thể HTML<
và>
.Ngoài ra, bạn có thể để các dấu ngoặc thô
<>
trong đoạn mã nếu gói các phần vi phạm trong{@code foo}
. Ví dụ:<pre>{@code <manifest>}</pre>
Tuân thủ hướng dẫn về kiểu tham chiếu API
Để đảm bảo tính nhất quán về kiểu cho phầ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 tài liệu cho công cụ Javadoc.
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()
Trình tạo ý định nên sử dụng các phương thức có tên là createFooIntent()
.
Sử dụng Gói thay vì tạo cấu trúc dữ liệu mới dùng cho nhiều mục đích
Tránh tạo cấu trúc dữ liệu mới dùng cho nhiều mục đích để biểu thị mối liên kết khoá tuỳ ý với giá trị đã 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 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 phải 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 thư viện Jetpack).
Trong trường hợp nền tảng có đọc dữ liệu, hãy tránh sử dụng Bundle
và ưu tiên sử dụng lớp dữ liệu được nhập phần lớn.
Các phương thức triển khai có thể phân phối phải có trường CREATOR công khai
Hoạt động tăng cường có thể phân phối đượ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 chuỗi giao diện người dùng
Khi một chuỗi được trình bày 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 số nhãn hoặc giá trị khác mà người dùng không nhìn thấy, thì bạn có thể sử dụng String
.
Tránh sử dụng Enums
Bạn phải sử dụng IntDef
thay vì enum trong tất cả API nền tảng và nên cân nhắc kỹ trong các API thư viện không được phân tách. Chỉ sử dụng enum khi bạn chắc chắn rằng giá trị mới sẽ không được thêm vào.
Lợi ích của IntDef
:
- Cho phép thêm giá trị theo thời gian
- Câu lệnh
when
của Kotlin có thể không thành công trong thời gian chạy nếu các câu lệnh đó không còn đầy đủ do một giá trị enum được thêm vào nền tảng.
- Câu lệnh
- Không có lớp hoặc đối tượng nào được sử dụng trong thời gian chạy, chỉ có các đối tượng gốc
- Mặc dù R8 hoặc tính năng rút gọn có thể tránh được chi phí này cho các API thư viện không được đóng gói, nhưng tính năng 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
- Bật nút chuyển toàn diện, sử dụng câu lệnh
when
- Lưu ý – các giá trị không được thay đổi theo thời gian, hãy xem danh sách trước đó
- Tên rõ ràng và dễ tìm
- Bật tính năng 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 giao diện, có trình trợ giúp tĩnh, hiển thị 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ệ phân cấp gói android.*
có thứ tự ngầm ẩn, 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 trung lập với nhà cung cấp. API phải chung chung và có thể được các nhà tích hợp hệ thống hoặc ứng dụng có các quyền cần thiết sử dụng như nhau.
Các phương thức triển khai Parcelable phải là phương thức cuối cùng
Các lớp có thể phân phối do nền tảng xác định luôn được tải từ framework.jar
, vì vậy, ứng dụng không được ghi đè quá trình triển khai Parcelable
.
Nếu ứng dụng gửi mở rộng Parcelable
, thì ứng dụng nhận sẽ không có cách triển khai tuỳ chỉnh của ứng dụng gửi để giải nén. Lưu ý về khả năng tương thích ngược: nếu trước đây lớp của bạn 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, thì 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 phải gửi lại RemoteException dưới dạng RuntimeException
RemoteException
thường do AIDL nội bộ gửi và cho biết rằng quy trình hệ thống đã bị buộc 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 phải gửi lại dưới dạng RuntimeException
để ngăn ứ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ã nguyên mẫu này là phương pháp hay nhất:
try {
...
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
Gửi các 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 giữa các cấp độ API và gây ra sự cố ứ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 một yêu cầu trước đó hợp lệ, hãy gửi một ngoại lệ cụ thể mới thay vì ngoại lệ chung. Ví dụ: ExportedFlagRequired
thay vì SecurityException
(và ExportedFlagRequired
có thể mở rộng SecurityException
).
Điều này sẽ giúp các nhà phát triển ứng dụng và công cụ phát hiện các thay đổi về hành vi của API.
Triển khai hàm khởi tạo 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 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 Builder để tạo nên nên cân nhắc việc thêm hàm khởi tạo sao chép của Builder để 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 đó, API phải 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 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 cách bạn có thể 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. Loại này có thể thay đổi, yêu cầu phân bổ cho các lệnh gọi phương thức tần suất cao và không cung cấp ý nghĩa ngữ nghĩa cho nội dung mà mỗi bit đại diện.
Đối với các trường hợp 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 có 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 sử 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 sử 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à tuyệt đối không sử dụng java.net.URL
vì định nghĩa về sự bằng nhau của hàm này bị lỗi nghiêm trọng.
Ẩn chú thích được đánh dấu là @IntDef, @LongDef hoặc @StringDef
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ác hằng số này được xuất dưới dạng 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 hữu ích) còn lại trong mã giả lập API của chú thích (đối với nền tảng) hoặc JAR (đối với thư viện).
Do đó, việc sử dụng các chú thích này phải được đánh dấu bằng chú thích tài liệu @hide
trong nền tảng hoặc chú thích mã @RestrictTo.Scope.LIBRARY)
trong thư viện. Bạn phải đánh dấu các tệp này là @Retention(RetentionPolicy.SOURCE)
trong cả hai trường hợp để ngăn các tệp này xuất hiện trong các mô-đun 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 các chú thích đó riêng biệt với các nguồn đã biên dịch. Android Studio sẽ đọc định dạng gói này và thực thi các định nghĩa loại.
Không thêm khoá nhà cung cấp chế độ cài đặt mới
Không hiển thị các khoá mới từ Settings.Global
, Settings.System
hoặc Settings.Secure
.
Thay vào đó, hãy thêm một API getter và setter Java thích hợp trong 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 để thông báo cho ứng dụng về các thay đổi nếu cần.
Chế độ cài đặt SettingsProvider
có một số vấn đề so với phương thức getter/setter:
- Không 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 thích hợp để tuỳ chỉnh quyền.
- Ví dụ: bạn không thể bảo vệ chế độ cài đặt bằng quyền tuỳ chỉnh.
- Không có cách nào phù hợp để thêm logic tuỳ chỉnh đúng cách.
- 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í đã ngừng sử dụng API này cho một API Java thích hợp LocationManager.isLocationEnabled()
và thông báo truyền tin MODE_CHANGED_ACTION
, giúp nhóm linh hoạt hơn rất nhiều và ngữ nghĩa của các API hiện rõ ràng hơn rất nhiều.
Không mở rộng Hoạt động và AsyncTask
AsyncTask
là thông tin chi tiết về cách triển khai. Thay vào đó, hãy hiển thị trình nghe hoặc trong androidx, API ListenableFuture
.
Không thể soạn các lớp con Activity
. Việc mở rộng hoạt động cho tính năng của bạn sẽ khiến tính năng đó không tương thích với các tính năng khác yêu cầu người dùng làm như vậy. Thay vào đó, hãy dựa vào thành phần kết hợp 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 liên kết với Context
, chẳng hạn như mọi nội dung được trả về từ Context.getSystemService()
, phải sử dụng người dùng liên kết với Context
thay vì hiển thị các thành viên nhắm đến 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);
}
}
Trường hợp ngoại lệ: Một phương thức có thể chấp nhận đối số người dùng nếu phương thức đó 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ì int thông thường
Bạn nên sử dụng UserHandle
để đảm bảo an toàn cho loại 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);
Trong trường hợp không thể tránh khỏi, int
đại diện cho mã nhận dạng người dùng phải được chú thích bằng @UserIdInt
.
Foobar getFoobarForUser(@UserIdInt int user);
Ưu tiên trình nghe hoặc lệnh gọi lại để truyền tin về ý định
Ý định truyền tin rất mạnh mẽ, nhưng chúng đã dẫn đến các hành vi mới xuất hiện có thể ảnh hưởng tiêu cực đến trạng thái của hệ thống. Vì vậy, bạn nên thêm ý định truyền tin 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 ý định truyền tin 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 có thể là kết quả dự kiến, nhưng việc này có thể dẫn đến việc hàng chục ứng dụng bị lỗi, ả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
, để điều phối 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 phân phố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 các hàng đợi này có thể bị quá tải và có thể không phân phối sự kiện của bạn kịp thời. Chúng tôi đã quan sát thấy một số hàng đợi truyền tin trong thực tế có độ trễ từ đầu đến cuối là 10 phút trở lên.
Vì những lý do này, bạn nên cân nhắ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 cho các tính năng mới.
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:
- Nếu có thể, hãy sử dụng
Intent.FLAG_RECEIVER_REGISTERED_ONLY
để giới hạn thông báo truyền tin của bạn cho các ứng dụng đang chạy. Ví dụ:ACTION_SCREEN_ON
sử dụng thiết kế này để tránh đánh thức ứng dụng. - Nếu có thể, hãy sử dụng
Intent.setPackage()
hoặcIntent.setComponent()
để nhắm mục tiêu thông báo truyền tin đến 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 việc ứng dụng hiện đang xử lý các chế độ điều khiển 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 liên kết với hệ thống
Các dịch vụ mà nhà phát triển dự định mở rộng và 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ụ đó phải đáp ứng các tiêu chí sau:
- Xác định 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ụ. Bạn phải chú thích hằng số này 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
để nhận Ý định từ nền tảng. - Bạn nên cân nhắc việc thêm quyền cấp hệ thống để ngăn các ứng dụng độc hại 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 của Android về khả năng tương tác Kotlin-Java để 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 để giúp tăng khả năng được khám phá.
Mức độ 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; tuy nhiên, đừng cố gắng kiểm soát chế độ hiển thị dành riêng cho ngôn ngữ bằng @JvmSynthetic
vì API này có các hiệu ứng phụ đối với cách API được trình bày trong 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 Kotlin-Java hoặc Hướng dẫn về tính năng không đồng bộ để 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 lớp 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ể hiển thị dưới dạng các lớp trống trong tệp văn bản API – lớp này đang 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 đồng hành 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 đó 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 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 giao diện API công khai đã hoàn thiện. 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ó các trường hợp hiếm gặp mà tính năng 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 API dựa trên Java của Quỹ Eclipse để biết nội dung giải thích chi tiết về những loại thay đổi API nào tương thích với Java. Các thay đổi có thể gây lỗi nhị phân trong API ẩn (ví dụ: hệ thống) phải tuân theo chu kỳ ngừng sử dụng/thay thế.
Thay đổi có thể gây lỗi nguồn
Bạn không nên thực hiện các thay đổi có thể gây lỗi nguồn ngay cả khi chúng 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 phá vỡ nguồn là thêm một lớp chung vào một lớp hiện có. Lớp này 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 tham chiếu không rõ ràng.
Các thay đổi gây lỗi 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 tìm hiểu 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 gây lỗi nguồn trở nên cần thiết để cải thiện trải nghiệm của nhà phát triển hoặc tính chính xác của mã. Ví dụ: việc thêm chú thích về tính chất rỗng vào nguồn Java sẽ 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 API riêng
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 lưu giữ các API được chú thích bằng @SystemApi
trong ba năm. Bạn phải xoá hoặc tái cấu trúc API hệ thống theo lịch sau:
- API y – Đã thêm
- API y+1 – Ngừng sử dụng
- Đánh dấu mã bằng
@Deprecated
. - Thêm 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 cách sử dụng chú thích tài liệu
@deprecated
. - Trong vòng đời phát triển, hãy báo cáo lỗi cho người dùng nội bộ để họ biết rằng API này sẽ không được dùng nữa. Điều này giúp xác thực rằng các API thay thế là đầy đủ.
- Đánh dấu mã bằng
- API y+2 – Xoá mềm
- Đánh dấu mã bằng
@removed
. - Bạn có thể gửi hoặc không gửi 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 ngừng sử dụng là một thay đổi về API và việc này có thể xảy ra trong một bản phát hành lớn (chẳng hạn như bản phát hành theo thư). Sử dụng chú giải nguồn @Deprecated
và chú giải tài liệu @deprecated
<summary>
cùng nhau khi ngừng sử dụng API. Bản tóm tắt phải bao gồm 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 ngừng sử 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 hiển thị trong lớp android.R
, với 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 sử dụng API
Thông báo ngừng hoạt động hữu ích nhất khi bạn không muốn người dùng sử dụng một API trong mã mới.
Chúng tôi cũng yêu cầu bạn đánh dấu API là @deprecated
trước khi API đó trở thành @removed
, nhưng điều này không tạo động lực mạnh mẽ để nhà phát triển di chuyển khỏi 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 có thể gây ra một số ảnh hưởng sau:
javac
phát ra cảnh báo trong quá trình biên dịch.- Bạn không thể ngăn chặn cảnh báo về việc ngừng sử dụng trên toàn cục hoặc đặt làm đường cơ sở, vì vậy, các nhà phát triển sử dụng
-Werror
cần khắc phục hoặc ngăn chặn riêng lẻ mọi trường hợp 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ể chặn cảnh báo về việc ngừng sử dụng khi nhập các lớp không dùng nữa. Do đó, nhà phát triển cần phải đưa tên lớp đủ điều kiện vào cùng dòng cho mọi trường hợp 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.
- Bạn không thể ngăn chặn cảnh báo về việc ngừng sử dụng trên toàn cục hoặc đặt làm đường cơ sở, 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 trang sử dụng API.
- IDE có thể hạ 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 mã (những nhà phát triển sử dụng -Werror
) không muốn sử dụng các SDK mới.
Những nhà phát triển không quan tâm đến cảnh báo trong mã hiện có có thể bỏ qua hoàn toàn việc ngừng sử dụng.
Một SDK giới thiệu một lượng lớn các 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ệ 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
@remove
API 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 ta không thể khắc phục mà không làm hỏng khả năng tương thích.
Khi bạn 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 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 các API không thể duy trì các hành vi được ghi nhận:
/**
* ...
* @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à việc 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 ngừng sử dụng API. Nếu API không có kiểm thử, bạn nên thêm kiểm thử.
Không mở rộng các nền tảng API không dùng nữa trong các bản phát hành trong tương lai. Bạn có thể thêm chú thích về tính chính xác của công cụ tìm lỗi mã nguồn (ví dụ: @Nullable
) vào một API hiện không dùng nữa, nhưng không nên thêm các API mới.
Không thêm API mới vào danh sách không dùng nữa. Nếu bất kỳ API nào đã được thêm và sau đó không dùng nữa trong một chu kỳ phát hành trước (do đó, ban đầu sẽ chuyển sang giao diện API công khai dưới dạng không dùng nữa), thì bạn phải xoá các API đó trước khi hoàn tất API.
Xoá tạm thời
Xoá mềm là một thay đổi có thể gây lỗi nguồn và bạn nên tránh việc này trong các API công khai, trừ phi Hội đồng API phê duyệt rõ ràng việc này.
Đối với 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á nhẹ. Xoá tất cả các tài liệu tham chiếu đến API và sử dụng chú thích tài liệu @removed <summary>
khi xoá nhẹ API. Phần tóm tắt phải nêu lý do xoá và có thể bao gồm cả chiến lược di chuyển, như chúng tôi đã giải thích trong phần Ngừng sử dụng.
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à phải duy trì hành vi đó để các phương thức gọi hiện có 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à giữ nguyên hành vi.
Bạn phải duy trì mức độ kiểm thử, nhưng nội dung của các chương trình kiểm thử có thể cần thay đổi để phù hợp với các thay đổi về hành vi. Các bài kiểm thử vẫn phải xác thực rằng các phương thức 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, bạn phải duy trì hành vi đó để các phương thức gọi hiện có 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à giữ nguyên hành vi.
Bạn phải duy trì mức độ kiểm thử, nhưng nội dung của các bài kiểm thử có thể cần thay đổi để phù hợp với các thay đổi về hành vi. Các bài kiểm thử vẫn phải xác thực rằng các phương thức gọi hiện có không gặp sự cố trong thời gian chạy.
Ở cấp độ kỹ thuật, chúng ta xoá API khỏi tệp JAR của mô-đun SDK và đường dẫn lớp thời gian biên dịch bằng chú thích Javadoc @remove
, nhưng API vẫn tồn tại trên đường dẫn lớp thời gian chạy – tương tự như các API @hide
:
/**
* Ringer volume. This is ...
*
* @removed Not functional since API 2.
*/
public static final String VOLUME_RING = ...
Từ quan điểm của nhà phát triển ứng dụng, API 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 mới hơn SDK mà API đã bị xoá; tuy nhiên, mã nguồn sẽ 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 tiếp tục hoạt động.
Một số danh mục API không được xoá mềm. Bạn không được xoá một số danh mục API nhất định.
Phương thức trừu tượng
Bạn không được xoá mềm các phương thức trừu tượng trên các 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 hiếm gặp mà nhà phát triển chưa bao giờ và sẽ không bao giờ có 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 phá vỡ tệp nhị phân và không bao giờ được xảy ra trong API công khai.
Chú thích 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 sử dụng 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 việc ngừng sử dụng. 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à 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 không được khuyến khích.
Thay đổi đối với 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
để thông báo rõ ràng khi nhà phát triển cố gắng đăng các sự kiện quá lớn để gửi qua Binder
.
Tuy nhiên, để tránh gây ra sự cố 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 đã chuyển sang yêu cầu sử dụng Khung tương thích ứng dụng. Dưới đây là ví dụ về cách triển khai thay đổi 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 bản dùng thử và bản phát hành beta trong quá trình gỡ lỗi ứng dụng, thay vì buộc họ phải điều chỉnh 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 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 đề thường gặp nhất về khả năng tương thích chuyển tiếp 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 đó được giả định là hoàn chỉnh (ví dụ: khiswitch
códefault
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ợ chỉ định tài nguyên loại
ColorStateList
trong XML, trong đó 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 các bước kiểm tra thời gian chạy, ví dụ: xoá bước kiểm tra
requireNotNull()
có trong các phiên bản thấp hơn.
Trong tất cả các trường hợp này, nhà phát triển chỉ phát hiện ra vấn đề khi chạy. Tệ hơn nữa, họ có thể phát hiện ra vấn đề này do báo cáo sự cố từ các thiết bị cũ trong thực tế.
Ngoài ra, tất cả các trường hợp này đều là thay đổi API hợp lệ về mặt kỹ thuật. Các lỗi này không làm gián đoạn khả năng tương thích của tệp nhị phân hoặc nguồn và công cụ tìm lỗi mã nguồn API sẽ không phát hiện được bất kỳ lỗi nào trong số này.
Do đó, nhà thiết kế API phải chú ý cẩn thận khi sửa đổi các lớp hiện có. Hãy đặt câu hỏi: "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 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 ngừng sử dụng một phần tử hoặc thuộc tính XML, bạn có thể thêm điểm đánh 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>
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 bạn muốn sửa đổi một loại hiện có, phương pháp hay nhất là 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>
Mẫu dành riêng cho dòng chính
Mainline là một dự án cho phép cập nhật từng hệ thống con ("mô-đun chính") của Android OS, thay vì cập nhật toàn bộ hình ảnh hệ thống.
Các mô-đun chính phải được "giải phóng" khỏi nền tảng cốt lõi, tức là tất cả các lượt tương tác giữa mỗi mô-đun và phần còn lại của thế giới phải được thực hiện bằ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ế nhất định mà các mô-đun chính phải tuân theo. Phần này mô tả các tính năng đó.
Mẫu <Module>FrameworkInitializer
Nếu 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 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 đếnContext
.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 đếnContext
.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ý đối tượng liên kết dịch vụ hệ thống hoặc tham chiếu đến các đối tượng đó, bạn 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ì đối tượng này bị ẩn. Lớp này bị ẩn vì các mô-đun chính không được đă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 hiển thị.
Thay vào đó, các mô-đun chính có thể sử dụng mẫu sau để có thể đăng ký và 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
, tuân 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 này từ các 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 bản sao hàm khởi tạo này.
- 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, bạn cần có một phương thức getter. Nếu có hai, 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 lớp đó đến một phương thức tĩnh do mô-đun hiển thị. Thông thường, bạn sẽ thêm một API@SystemApi(client = MODULE_LIBRARIES)
tĩnh trong lớpFrameworkInitializer
để lấy API đó.
Mẫu này sẽ ngăn các mô-đun chính khác truy cập vào các API này vì các mô-đun khác không có cách nào để lấy một thực thể của <YourModule>ServiceManager
, mặc dù các API get()
và register()
có thể hiển thị với các mô-đun đó.
Dưới đâ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 đối tượng liên kết dịch vụ trong mã gốc, bạn sẽ sử dụng API gốc AServiceManager
.
Các API này tương ứng với API Java ServiceManager
, nhưng các API gốc được hiển thị trực tiếp cho các mô-đun chính. Đừ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ị đối tượng liên kết từ mã gốc, thì <YourModule>ServiceManager.ServiceRegisterer
không cần phương thức register()
.
Định nghĩa về quyền trong mô-đun Mainline
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 của chúng giống như 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 đó 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 được cung cấp trong một API nền tảng có thể cập nhật cho các ứng dụng khác, thì tên quyền phải có tiền tố là "android.permission". (chẳng hạn như mọi quyền nền tảng tĩnh) cùng với tên gói mô-đun, để cho biết đó là API nền tảng từ một mô-đun, đồng thời 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
.