این صفحه به عنوان راهنمایی برای توسعهدهندگان در نظر گرفته شده است تا اصول کلی که شورای API در بررسیهای API اعمال میکند را درک کنند.
علاوه بر رعایت این دستورالعملها هنگام نوشتن APIها، توسعهدهندگان باید ابزار API Lint را اجرا کنند که بسیاری از این قوانین را در بررسیهایی که در برابر APIها اجرا میشوند، کدگذاری میکند.
این را به عنوان راهنمایی برای قوانینی که توسط آن ابزار Lint رعایت میشوند، به علاوه توصیههای کلی در مورد قوانینی که نمیتوان آنها را با دقت بالا در آن ابزار کدگذاری کرد، در نظر بگیرید.
ابزار API Lint
API Lint در ابزار تحلیل استاتیک Metalava ادغام شده است و به طور خودکار در طول اعتبارسنجی در CI اجرا میشود. میتوانید آن را به صورت دستی از یک پرداخت پلتفرم محلی با استفاده از m checkapi یا یک پرداخت محلی AndroidX با استفاده از ./gradlew :path:to:project:checkApi اجرا کنید.
قوانین API
پلتفرم اندروید و بسیاری از کتابخانههای Jetpack قبل از ایجاد این مجموعه دستورالعملها وجود داشتند و سیاستهایی که در ادامه این صفحه آمدهاند، به طور مداوم در حال تکامل هستند تا نیازهای اکوسیستم اندروید را برآورده کنند.
در نتیجه، برخی از APIهای موجود ممکن است از دستورالعملها پیروی نکنند. در موارد دیگر، اگر یک API جدید به جای رعایت دقیق دستورالعملها، با APIهای موجود سازگار بماند، ممکن است تجربه کاربری بهتری برای توسعهدهندگان برنامه فراهم کند.
اگر سوالات دشواری در مورد API وجود دارد که باید حل شوند یا دستورالعملهایی که نیاز به بهروزرسانی دارند، از قضاوت خود استفاده کنید و با شورای API تماس بگیرید.
اصول اولیه API
این دسته مربوط به جنبههای اصلی یک API اندروید است.
تمام APIها باید پیادهسازی شوند.
صرف نظر از مخاطبان یک API (مثلاً عمومی یا @SystemApi )، تمام سطوح API باید هنگام ادغام یا نمایش به عنوان API پیادهسازی شوند. Stubهای API را با پیادهسازیهایی که قرار است بعداً ارائه شوند، ادغام نکنید .
سطوح API بدون پیادهسازی، مشکلات متعددی دارند:
- هیچ تضمینی وجود ندارد که یک سطح مناسب یا کامل در معرض دید قرار گرفته باشد. تا زمانی که یک API توسط کلاینتها آزمایش یا استفاده نشود، هیچ راهی برای تأیید اینکه کلاینت APIهای مناسب برای استفاده از آن ویژگی را دارد، وجود ندارد.
- APIهای بدون پیادهسازی را نمیتوان در پیشنمایشهای توسعهدهندگان آزمایش کرد.
- API های بدون پیاده سازی را نمی توان در CTS آزمایش کرد.
تمام APIها باید آزمایش شوند
این با الزامات CTS پلتفرم، سیاستهای AndroidX و بهطورکلی این ایده که APIها باید پیادهسازی شوند، مطابقت دارد.
آزمایش سطوح API تضمین اساسی ارائه میدهد که سطح API قابل استفاده است و ما موارد استفاده مورد انتظار را بررسی کردهایم. آزمایش وجود کافی نیست؛ رفتار خود API نیز باید آزمایش شود.
تغییری که یک API جدید اضافه میکند، باید شامل تستهای مربوطه در همان موضوع CL یا Gerrit باشد.
APIها همچنین باید قابل آزمایش باشند. شما باید بتوانید به این سوال پاسخ دهید که «یک توسعهدهنده اپلیکیشن چگونه کدی را که از API شما استفاده میکند، آزمایش میکند؟»
تمام APIها باید مستندسازی شوند.
مستندسازی بخش کلیدی کاربردپذیری API است. اگرچه ممکن است سینتکس یک سطح API واضح به نظر برسد، اما هیچ مشتری جدیدی معناشناسی، رفتار یا زمینه پشت API را درک نخواهد کرد.
تمام API های تولید شده باید مطابق با دستورالعمل ها باشند.
API های تولید شده توسط ابزارها باید از همان دستورالعملهای API که در کدهای دستنویس وجود دارد، پیروی کنند.
ابزارهایی که برای تولید API توصیه نمیشوند:
-
AutoValue: به طرق مختلف دستورالعملها را نقض میکند، برای مثال، هیچ راهی برای پیادهسازی کلاسهای مقدار نهایی یا سازندگان نهایی با روشی که AutoValue کار میکند، وجود ندارد.
سبک کد
این دسته مربوط به سبک کد عمومی است که توسعهدهندگان باید از آن استفاده کنند، به خصوص هنگام نوشتن APIهای عمومی.
از قراردادهای استاندارد کدنویسی پیروی کنید، مگر در مواردی که ذکر شده باشد
قراردادهای کدنویسی اندروید برای مشارکتکنندگان خارجی در اینجا مستند شدهاند:
https://source.android.com/source/code-style.html
در مجموع، ما تمایل داریم از قراردادهای استاندارد کدنویسی جاوا و کاتلین پیروی کنیم.
کلمات اختصاری نباید در نام متدها با حروف بزرگ نوشته شوند
برای مثال: نام متد باید runCtsTests باشد و نه runCTSTests .
اسمها نباید به Impl ختم شوند.
این کار جزئیات پیادهسازی را افشا میکند، از آن اجتناب کنید.
کلاسها
این بخش قوانین مربوط به کلاسها، رابطها و وراثت را شرح میدهد.
کلاسهای عمومی جدید را از کلاس پایه مناسب به ارث ببرید
وراثت، عناصر API را در زیرکلاس شما که ممکن است مناسب نباشند، در معرض نمایش قرار میدهد. برای مثال، یک زیرکلاس عمومی جدید از FrameLayout شبیه FrameLayout به علاوه رفتارها و عناصر API جدید است. اگر آن API ارثبری شده برای مورد استفاده شما مناسب نیست، از یک کلاس بالاتر در درخت، مثلاً ViewGroup یا View ، ارثبری کنید.
اگر وسوسه شدید که متدهای کلاس پایه را برای ایجاد خطای UnsupportedOperationException نادیده بگیرید، در مورد کلاس پایهای که استفاده میکنید تجدید نظر کنید.
از کلاسهای مجموعههای پایه استفاده کنید
چه بخواهید یک مجموعه را به عنوان آرگومان بگیرید و چه بخواهید آن را به عنوان مقدار برگردانید، همیشه کلاس پایه را به پیادهسازی خاص (مانند return List<Foo> به جای ArrayList<Foo> ) ترجیح دهید.
از یک کلاس پایه استفاده کنید که محدودیتهای مناسبی را برای API بیان کند. برای مثال، List برای API که مجموعه آن باید مرتب باشد استفاده کنید و Set برای API که مجموعه آن باید شامل عناصر منحصر به فرد باشد استفاده کنید.
در کاتلین، مجموعههای تغییرناپذیر را ترجیح دهید. برای جزئیات بیشتر به بخش تغییرپذیری مجموعه مراجعه کنید.
کلاسهای انتزاعی در مقابل رابطها
جاوا ۸ از متدهای پیشفرض رابط کاربری پشتیبانی میکند که به طراحان API اجازه میدهد متدهایی را به رابطها اضافه کنند و در عین حال سازگاری باینری را حفظ کنند. کد پلتفرم و تمام کتابخانههای Jetpack باید جاوا ۸ یا بالاتر را هدف قرار دهند.
در مواردی که پیادهسازی پیشفرض بدون وضعیت (stateless) است، طراحان API باید رابطها را به کلاسهای انتزاعی ترجیح دهند -- یعنی، متدهای پیشفرض رابط میتوانند به عنوان فراخوانیهایی به سایر متدهای رابط پیادهسازی شوند.
در مواردی که پیادهسازی پیشفرض به سازنده یا حالت داخلی نیاز دارد، باید از کلاسهای انتزاعی استفاده شود.
در هر دو مورد، طراحان API میتوانند برای سادهسازی استفاده به عنوان یک لامبدا، یک متد واحد را انتزاعی باقی بگذارند:
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) { }
}
نام کلاسها باید منعکسکنندهی چیزی باشد که آنها را بسط میدهند.
برای مثال، کلاسهایی که Service ارثبری میکنند، برای وضوح بیشتر باید FooService نامگذاری شوند:
public class IntentHelper extends Service {}
public class IntentService extends Service {}
پسوندهای عمومی
از استفاده از پسوندهای نام کلاس عمومی مانند Helper و Util برای مجموعه متدهای کاربردی خودداری کنید. در عوض، متدها را مستقیماً در کلاسهای مرتبط یا در توابع افزونه کاتلین قرار دهید.
در مواردی که متدها چندین کلاس را به هم متصل میکنند، به کلاس حاوی آن یک نام معنادار بدهید که عملکرد آن را توضیح دهد.
در موارد بسیار محدود، استفاده از پسوند Helper ممکن است مناسب باشد:
- برای ترکیب رفتار پیشفرض استفاده میشود
- ممکن است شامل واگذاری رفتار موجود به کلاسهای جدید باشد
- ممکن است به حالت پایدار نیاز داشته باشد
- معمولاً شامل
Viewمیشود.
برای مثال، اگر بکپورت کردن tooltipها نیاز به حفظ وضعیت مرتبط با یک View و فراخوانی چندین متد در View برای نصب بکپورت داشته باشد، TooltipHelper نام کلاس قابل قبولی خواهد بود.
کد تولید شده توسط IDL را مستقیماً به عنوان API های عمومی نمایش ندهید
کد تولید شده توسط IDL را به عنوان جزئیات پیادهسازی نگه دارید. این شامل protobuf، sockets، FlatBuffers یا هر سطح API غیر جاوا و غیر NDK دیگری میشود. با این حال، بیشتر IDL در اندروید در AIDL است، بنابراین این صفحه بر AIDL تمرکز دارد.
کلاسهای AIDL تولید شده الزامات راهنمای سبک API را برآورده نمیکنند (برای مثال، نمیتوانند از overloading استفاده کنند) و ابزار AIDL به صراحت برای حفظ سازگاری API زبان طراحی نشده است، بنابراین نمیتوانید آنها را در یک API عمومی جاسازی کنید.
در عوض، یک لایه API عمومی روی رابط AIDL اضافه کنید، حتی اگر در ابتدا یک پوشش کمعمق باشد.
رابطهای اتصالدهنده
اگر رابط Binder یک جزئیات پیادهسازی باشد، میتوان آن را در آینده آزادانه تغییر داد، و لایه عمومی امکان حفظ سازگاری معکوس مورد نیاز را فراهم میکند. برای مثال، ممکن است لازم باشد آرگومانهای جدیدی به فراخوانیهای داخلی اضافه کنید، یا ترافیک IPC را با استفاده از دسته بندی یا جریان، استفاده از حافظه مشترک یا موارد مشابه بهینه کنید. اگر رابط AIDL شما API عمومی نیز باشد، هیچ یک از این کارها قابل انجام نیست.
برای مثال، FooService مستقیماً به عنوان یک API عمومی نمایش ندهید:
// BAD: Public API generated from IFooService.aidl
public class IFooService {
public void doFoo(String foo);
}
در عوض، رابط Binder را درون یک کلاس manager یا کلاس دیگری قرار دهید:
/**
* @hide
*/
public class IFooService {
public void doFoo(String foo);
}
public IFooManager {
public void doFoo(String foo) {
mFooService.doFoo(foo);
}
}
اگر بعداً به آرگومان جدیدی برای این فراخوانی نیاز باشد، رابط داخلی میتواند حداقل باشد و overloadهای مناسب به API عمومی اضافه شوند. میتوانید از لایه پوششی برای مدیریت سایر نگرانیهای مربوط به سازگاری با نسخههای قبلی در حین تکامل پیادهسازی استفاده کنید:
/**
* @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);
}
}
برای رابطهای Binder که بخشی از پلتفرم اندروید نیستند (برای مثال، یک رابط سرویس که توسط سرویسهای Google Play برای استفاده برنامهها صادر میشود)، الزام به یک رابط IPC پایدار، منتشر شده و نسخهبندی شده به این معنی است که تکامل خود رابط بسیار دشوارتر است. با این حال، هنوز هم ارزشمند است که یک لایه پوششی در اطراف آن داشته باشید تا با سایر دستورالعملهای API مطابقت داشته باشد و استفاده از همان API عمومی را برای نسخه جدید رابط IPC، در صورت لزوم، آسانتر کند.
از اشیاء خام Binder در API عمومی استفاده نکنید
یک شیء Binder به خودی خود هیچ معنایی ندارد و بنابراین نباید در API عمومی استفاده شود. یکی از موارد استفاده رایج، استفاده از Binder یا IBinder به عنوان توکن است زیرا دارای معنای هویتی است. به جای استفاده از یک شیء Binder خام، از یک کلاس توکن wrapper استفاده کنید.
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() {...}
}
کلاسهای مدیران باید نهایی باشند
کلاسهای مدیر باید به صورت final تعریف شوند. کلاسهای مدیر با سرویسهای سیستم ارتباط برقرار میکنند و تنها نقطه تعامل هستند. نیازی به سفارشیسازی نیست، بنابراین آن را به صورت final تعریف کنید.
از CompletableFuture یا Future استفاده نکنید
java.util.concurrent.CompletableFuture دارای یک سطح API بزرگ است که امکان جهش دلخواه در مقدار آینده را فراهم میکند و دارای پیشفرضهای مستعد خطا است.
برعکس، java.util.concurrent.Future فاقد قابلیت شنود غیر مسدودکننده است و استفاده از آن را با کد ناهمزمان دشوار میکند.
در کد پلتفرم و APIهای کتابخانهای سطح پایین که توسط کاتلین و جاوا استفاده میشوند ، ترکیبی از یک فراخوانی برگشتی تکمیل، Executor و در صورت پشتیبانی API از لغو، CancellationSignal ترجیح دهید.
public void asyncLoadFoo(android.os.CancellationSignal cancellationSignal,
Executor callbackExecutor,
android.os.OutcomeReceiver<FooResult, Throwable> callback);
اگر کاتلین را هدف قرار میدهید، توابع suspend ترجیح دهید.
suspend fun asyncLoadFoo(): Foo
در کتابخانههای یکپارچهسازی مخصوص جاوا ، میتوانید ListenableFuture شرکت Guava استفاده کنید.
public com.google.common.util.concurrent.ListenableFuture<Foo> asyncLoadFoo();
از گزینه اختیاری استفاده نکنید
اگرچه Optional میتواند در برخی سطوح API مزایایی داشته باشد، اما با سطح API موجود اندروید سازگار نیست. @Nullable و @NonNull کمک ابزاری برای امنیت null ارائه میدهند و کاتلین قراردادهای nullability را در سطح کامپایلر اعمال میکند و Optional را غیرضروری میسازد.
برای مقادیر اولیه اختیاری، از متدهای has و get جفت شده استفاده کنید. اگر مقداری تنظیم نشده باشد ( has false را برمیگرداند)، متد get باید یک IllegalStateException صادر کند.
public boolean hasAzimuth() { ... }
public int getAzimuth() {
if (!hasAzimuth()) {
throw new IllegalStateException("azimuth is not set");
}
return azimuth;
}
استفاده از سازندههای خصوصی برای کلاسهای غیرقابل نمونهسازی
کلاسهایی که فقط میتوانند توسط Builder ها ایجاد شوند، کلاسهایی که فقط شامل ثابتها یا متدهای استاتیک هستند، یا کلاسهای غیرقابل نمونهسازی، باید حداقل یک سازندهی خصوصی داشته باشند تا از نمونهسازی با استفاده از سازندهی پیشفرض بدون آرگومان جلوگیری شود.
public final class Log {
// Not instantiable.
private Log() {}
}
سینگلتونها
Singleton به دلیل داشتن معایب زیر در رابطه با تست، توصیه نمیشود:
- ساخت و ساز توسط کلاس مدیریت میشود و از استفاده از موارد تقلبی جلوگیری میشود.
- به دلیل ماهیت استاتیک یک singleton، تستها نمیتوانند هرمتیک باشند.
- برای حل این مشکلات، توسعهدهندگان یا باید جزئیات داخلی سینگلتون را بدانند یا یک پوشش (wrapper) برای آن ایجاد کنند.
الگوی تک نمونهای را ترجیح دهید، که برای حل این مشکلات به یک کلاس پایه انتزاعی متکی است.
نمونه واحد
کلاسهای تک نمونهای از یک کلاس پایه انتزاعی با سازنده private یا internal استفاده میکنند و یک متد استاتیک getInstance() برای دریافت یک نمونه ارائه میدهند. متد getInstance() باید در فراخوانیهای بعدی، همان شیء را برگرداند.
شیء برگردانده شده توسط getInstance() باید یک پیادهسازی خصوصی از کلاس پایه انتزاعی باشد.
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
}
}
}
نمونه واحد با نمونه واحد (singleton) متفاوت است، زیرا توسعهدهندگان میتوانند یک نسخه جعلی از SingleInstance ایجاد کنند و از چارچوب تزریق وابستگی خود برای مدیریت پیادهسازی بدون نیاز به ایجاد یک پوشش استفاده کنند، یا کتابخانه میتواند نسخه جعلی خود را در یک مصنوع -testing ارائه دهد.
کلاسهایی که منابع را آزاد میکنند باید AutoCloseable را پیادهسازی کنند.
کلاسهایی که منابع را از طریق close ، release ، destroy یا متدهای مشابه آزاد میکنند، باید java.lang.AutoCloseable پیادهسازی کنند تا به توسعهدهندگان اجازه دهند هنگام استفاده از بلوک try-with-resources این منابع را بهطور خودکار پاک کنند.
از معرفی زیرکلاسهای جدید View در اندروید خودداری کنید.*
کلاسهای جدیدی که به طور مستقیم یا غیرمستقیم از android.view.View در API عمومی پلتفرم (یعنی در android.* ) ارثبری میکنند، معرفی نکنید.
جعبه ابزار رابط کاربری اندروید اکنون Compose-first است. ویژگیهای جدید رابط کاربری که توسط پلتفرم ارائه میشوند، باید به عنوان APIهای سطح پایینتر ارائه شوند که میتوانند برای پیادهسازی Jetpack Compose و به صورت اختیاری اجزای رابط کاربری مبتنی بر View برای توسعهدهندگان در کتابخانههای Jetpack استفاده شوند. ارائه این اجزا در کتابخانهها، فرصتهایی را برای پیادهسازیهای backported در زمانی که ویژگیهای پلتفرم در دسترس نیستند، فراهم میکند.
فیلدها
این قوانین مربوط به فیلدهای عمومی در کلاسها هستند.
فیلدهای خام را نمایش ندهید
کلاسهای جاوا نباید فیلدها را مستقیماً در معرض نمایش قرار دهند. فیلدها باید خصوصی باشند و فقط با استفاده از getterها و setterهای عمومی، صرف نظر از اینکه این فیلدها final هستند یا خیر، قابل دسترسی باشند.
استثنائات نادر شامل ساختارهای داده پایه است که در آنها نیازی به بهبود رفتار تعیین یا بازیابی یک فیلد نیست. در چنین مواردی، فیلدها باید با استفاده از قراردادهای استاندارد نامگذاری متغیر، به عنوان مثال، Point.x و Point.y نامگذاری شوند.
کلاسهای کاتلین میتوانند ویژگیها (properties) را در معرض نمایش قرار دهند.
فیلدهای نمایش داده شده باید به صورت نهایی علامت گذاری شوند.
فیلدهای خام اکیداً توصیه نمیشوند (@see Don’t appear raw fields ). اما در موارد نادری که یک فیلد به عنوان یک فیلد عمومی در معرض نمایش قرار میگیرد، آن فیلد را final علامت بزنید.
فیلدهای داخلی نباید در معرض دید قرار گیرند
در API عمومی به نام فیلدهای داخلی ارجاع ندهید.
public int mFlags;
به جای protected از public استفاده کنید
@see به جای protected از public استفاده کنید
ثابتها
اینها قوانینی در مورد ثابتهای عمومی هستند.
ثابتهای پرچم نباید با مقادیر int یا long همپوشانی داشته باشند.
پرچمها به بیتهایی اشاره دارند که میتوانند با هم ترکیب شده و مقداری از نوع union value را تشکیل دهند. اگر اینطور نیست، متغیر یا ثابت 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;
برای اطلاعات بیشتر در مورد تعریف ثابتهای پرچم عمومی، به @IntDef برای پرچمهای bitmask مراجعه کنید.
ثابتهای ثابت ثابت باید از قرارداد نامگذاری با حروف بزرگ و جدا شده با زیرخط استفاده کنند.
تمام کلمات در ثابت باید با حروف بزرگ نوشته شوند و کلمات چندگانه باید با _ از هم جدا شوند. به عنوان مثال:
public static final int fooThing = 5
public static final int FOO_THING = 5
استفاده از پیشوندهای استاندارد برای ثابتها
بسیاری از ثابتهای مورد استفاده در اندروید برای موارد استاندارد مانند پرچمها، کلیدها و اقدامات هستند. این ثابتها باید پیشوندهای استانداردی داشته باشند تا به عنوان این موارد قابل شناساییتر باشند.
برای مثال، intent extras باید با EXTRA_ شروع شود. اکشنهای Intent باید با ACTION_ شروع شوند. ثابتهایی که با Context.bindService() استفاده میشوند باید با BIND_ شروع شوند.
نامها و حوزههای ثابت کلیدی
مقادیر ثابت رشتهای باید با نام خود ثابت سازگار باشند و عموماً باید در محدودهی بسته یا دامنه قرار گیرند. برای مثال:
public static final String FOO_THING = "foo"
نه به طور مداوم نامگذاری شده و نه به طور مناسب دامنه بندی شده است. در عوض، موارد زیر را در نظر بگیرید:
public static final String FOO_THING = "android.fooservice.FOO_THING"
پیشوندهای android در ثابتهای رشتهای scoped برای پروژه متنباز اندروید رزرو شدهاند.
اکشنها و افزونههای Intent و همچنین ورودیهای Bundle باید با استفاده از نام پکیجی که در آن تعریف شدهاند، نامگذاری شوند.
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"
}
به جای protected از public استفاده کنید
@see به جای protected از public استفاده کنید
از پیشوندهای ثابت استفاده کنید
ثابتهای مرتبط باید همگی با پیشوند یکسانی شروع شوند. برای مثال، برای مجموعهای از ثابتها که با مقادیر پرچم استفاده میشوند:
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 از پیشوندهای استاندارد برای ثابتها استفاده کنید
از نامهای منابع سازگار استفاده کنید
شناسهها، ویژگیها و مقادیر عمومی باید با استفاده از قرارداد نامگذاری camelCase نامگذاری شوند، برای مثال @id/accessibilityActionPageUp یا @attr/textAppearance ، مشابه فیلدهای عمومی در جاوا.
در برخی موارد، یک شناسه یا ویژگی عمومی شامل یک پیشوند مشترک است که با یک زیرخط از هم جدا شده است:
- مقادیر پیکربندی پلتفرم مانند
@string/config_recentsComponentNameدر config.xml - ویژگیهای نمای مختص طرحبندی مانند
@attr/layout_marginStartدر attrs.xml
تمها و استایلهای عمومی باید از قرارداد نامگذاری سلسله مراتبی PascalCase پیروی کنند، برای مثال @style/Theme.Material.Light.DarkActionBar یا @style/Widget.Material.SearchView.ActionBar ، مشابه کلاسهای تو در تو در جاوا.
منابع طرحبندی و ترسیمپذیر نباید به عنوان APIهای عمومی نمایش داده شوند. با این حال، اگر قرار است نمایش داده شوند، طرحبندیها و ترسیمپذیرهای عمومی باید با استفاده از قرارداد نامگذاری under_score نامگذاری شوند، به عنوان مثال layout/simple_list_item_1.xml یا drawable/title_bar_tall.xml .
وقتی ثابتها میتوانند تغییر کنند، آنها را پویا کنید
کامپایلر ممکن است مقادیر ثابت را درونخطی کند، بنابراین ثابت نگهداشتن مقادیر بخشی از قرارداد API محسوب میشود. اگر مقدار ثابتهای MIN_FOO یا MAX_FOO ممکن است در آینده تغییر کند، به جای آن، تبدیل آنها به متدهای پویا را در نظر بگیرید.
CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()
سازگاری رو به جلو را برای فراخوانیهای برگشتی در نظر بگیرید
ثابتهای تعریفشده در نسخههای آیندهی API برای برنامههایی که APIهای قدیمیتر را هدف قرار میدهند، شناختهشده نیستند. به همین دلیل، ثابتهای ارائهشده به برنامهها باید نسخهی API هدف آن برنامه را در نظر بگیرند و ثابتهای جدیدتر را به یک مقدار ثابت نگاشت کنند. سناریوی زیر را در نظر بگیرید:
منبع فرضی SDK:
// 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;
برنامه فرضی با targetSdkVersion="22" :
if (result == STATUS_FAILURE) {
// Oh no!
} else {
// Success!
}
در این مورد، برنامه با محدودیتهای API سطح ۲۲ طراحی شده و فرض (تا حدودی) معقولی را در نظر گرفته است که فقط دو حالت ممکن وجود دارد. با این حال، اگر برنامه STATUS_FAILURE_RETRY که به تازگی اضافه شده است را دریافت کند، این را به عنوان موفقیت تفسیر میکند.
متدهایی که ثابتها را برمیگردانند میتوانند با محدود کردن خروجی خود برای مطابقت با سطح API مورد نظر برنامه، مواردی مانند این را با خیال راحت مدیریت کنند:
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;
}
توسعهدهندگان نمیتوانند پیشبینی کنند که آیا فهرستی از ثابتها ممکن است در آینده تغییر کند یا خیر. اگر شما یک API را با یک ثابت UNKNOWN یا UNSPECIFIED تعریف کنید که شبیه یک «همه چیز» (catch-all) به نظر میرسد، توسعهدهندگان فرض میکنند که ثابتهای منتشر شده هنگام نوشتن برنامهشان جامع هستند. اگر تمایلی به تعیین این انتظار ندارید، بررسی کنید که آیا یک ثابت همه چیز برای API شما ایده خوبی است یا خیر.
علاوه بر این، کتابخانهها نمیتوانند targetSdkVersion خود را جدا از برنامه مشخص کنند و مدیریت تغییرات رفتار targetSdkVersion از کد کتابخانه پیچیده و مستعد خطا است.
ثابت عدد صحیح یا رشتهای
اگر فضای نام برای مقادیر خارج از بسته شما قابل توسعه نیست، از ثابتهای عدد صحیح و @IntDef استفاده کنید. اگر فضای نام به اشتراک گذاشته شده است یا میتواند توسط کد خارج از بسته شما گسترش یابد، از ثابتهای رشتهای استفاده کنید.
کلاسهای داده
کلاسهای داده مجموعهای از ویژگیهای تغییرناپذیر را نشان میدهند و مجموعهای کوچک و خوشتعریف از توابع کاربردی را برای تعامل با آن دادهها فراهم میکنند.
data class در APIهای عمومی کاتلین استفاده نکنید ، زیرا کامپایلر کاتلین سازگاری API زبان یا باینری را برای کد تولید شده تضمین نمیکند. در عوض، توابع مورد نیاز را به صورت دستی پیادهسازی کنید.
نمونهسازی
در جاوا، کلاسهای داده باید وقتی تعداد کمی ویژگی وجود دارد، یک سازنده ارائه دهند یا وقتی تعداد زیادی ویژگی وجود دارد، از الگوی Builder استفاده کنند.
در کاتلین، کلاسهای داده باید صرف نظر از تعداد ویژگیها، یک سازنده با آرگومانهای پیشفرض ارائه دهند. کلاسهای داده تعریف شده در کاتلین همچنین ممکن است از ارائه یک سازنده هنگام هدف قرار دادن کلاینتهای جاوا بهرهمند شوند.
اصلاح و کپی کردن
در مواردی که دادهها نیاز به اصلاح دارند، یا یک سازنده کپی (جاوا) یا یک تابع عضو copy() (کاتلین) که یک شیء جدید برمیگرداند، به یک کلاس Builder ارائه دهید.
هنگام ارائه یک تابع copy() در کاتلین، آرگومانها باید با سازنده کلاس مطابقت داشته باشند و مقادیر پیشفرض باید با استفاده از مقادیر فعلی شیء پر شوند:
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
)
}
رفتارهای اضافی
کلاسهای داده باید هر دو equals() و hashCode() را پیادهسازی کنند و هر ویژگی باید در پیادهسازی این متدها در نظر گرفته شود.
کلاسهای داده میتوانند toString() با فرمت پیشنهادی مطابق با پیادهسازی کلاس داده کاتلین پیادهسازی کنند، برای مثال User(var1=Alex, var2=42) .
روشها
اینها قوانینی در مورد جزئیات مختلف در متدها، پیرامون پارامترها، نام متدها، انواع برگشتی و تصریحکنندههای دسترسی هستند.
زمان
این قوانین نحوه بیان مفاهیم زمانی مانند تاریخ و مدت زمان در APIها را پوشش میدهند.
در صورت امکان، نوعهای java.time.* را ترجیح دهید.
java.time.Duration ، java.time.Instant و بسیاری دیگر از انواع java.time.* از طریق desugaring در تمام نسخههای پلتفرم در دسترس هستند و هنگام بیان زمان در پارامترهای API یا مقادیر بازگشتی باید ترجیح داده شوند.
ترجیح میدهم فقط انواعی از API را نمایش دهم که java.time.Duration یا java.time.Instant را میپذیرند یا برمیگردانند و انواع اولیه با مورد استفاده مشابه را حذف کنم، مگر اینکه دامنه API دامنهای باشد که تخصیص شیء در الگوهای استفاده مورد نظر، تأثیر منفی بر عملکرد داشته باشد.
متدهایی که مدت زمان را بیان میکنند باید با نام duration نامگذاری شوند.
اگر یک مقدار زمانی، مدت زمان مربوطه را بیان میکند، پارامتر را «مدت زمان» بنامید، نه «زمان».
ValueAnimator.setTime(java.time.Duration);
ValueAnimator.setDuration(java.time.Duration);
استثنائات:
«timeout» زمانی مناسب است که مدت زمان به طور خاص به یک مقدار timeout اعمال شود.
«زمان» با نوع java.time.Instant برای اشاره به یک نقطه خاص در زمان مناسب است، نه یک مدت زمان.
روشهایی که مدت زمان یا زمان را به عنوان یک متغیر اولیه بیان میکنند، باید با واحد زمانی خود نامگذاری شوند و از عبارت long استفاده کنند.
متدهایی که مدت زمان را به عنوان یک مقدار اولیه میپذیرند یا برمیگردانند، باید نام متد را با واحدهای زمانی مرتبط (مانند Millis ، Nanos ، Seconds ) پسوند دهند تا نام بدون تزئین برای استفاده با java.time.Duration رزرو شود. به Time مراجعه کنید.
روشها همچنین باید به طور مناسب با واحد و مبنای زمانی خود حاشیهنویسی شوند:
-
@CurrentTimeMillisLong: مقدار یک مهر زمانی غیرمنفی است که به صورت تعداد میلیثانیهها از تاریخ 1970-01-01T00:00:00Z اندازهگیری میشود. -
@CurrentTimeSecondsLong: مقدار یک مهر زمانی غیرمنفی است که به صورت تعداد ثانیهها از تاریخ 1970-01-01T00:00:00Z اندازهگیری میشود. -
@DurationMillisLong: مقدار، مدت زمان غیرمنفی بر حسب میلیثانیه است. -
@ElapsedRealtimeLong: مقدار یک مهر زمانی غیرمنفی در مبنای زمانSystemClock.elapsedRealtime()است. -
@UptimeMillisLong: مقدار یک مهر زمانی غیرمنفی در مبنای زمانیSystemClock.uptimeMillis()است.
پارامترهای زمان اولیه یا مقادیر بازگشتی باید long استفاده کنند، نه int .
ValueAnimator.setDuration(@DurationMillisLong long);
ValueAnimator.setDurationNanos(long);
روشهای بیانکننده واحدهای زمان باید ترجیحاً از نامهای واحد کوتاهشده و غیرمختصر استفاده کنند.
public void setIntervalNs(long intervalNs);
public void setTimeoutUs(long timeoutUs);
public void setIntervalNanos(long intervalNanos);
public void setTimeoutMicros(long timeoutMicros);
حاشیهنویسی آرگومانهای طولانی
این پلتفرم شامل چندین حاشیهنویسی است تا تایپ قویتری برای واحدهای زمانی با نوع long ارائه دهد:
-
@CurrentTimeMillisLong: مقدار یک مهر زمانی غیرمنفی است که به صورت تعداد میلیثانیهها از تاریخ1970-01-01T00:00:00Zاندازهگیری میشود، بنابراین در مبنای زمانیSystem.currentTimeMillis()قرار دارد. -
@CurrentTimeSecondsLong: مقدار یک مهر زمانی غیرمنفی است که به صورت تعداد ثانیهها از1970-01-01T00:00:00Zاندازهگیری میشود. -
@DurationMillisLong: مقدار، مدت زمان غیرمنفی بر حسب میلیثانیه است. -
@ElapsedRealtimeLong: مقدار یک مهر زمانی غیرمنفی در مبنای زمانSystemClock#elapsedRealtime()است. -
@UptimeMillisLong: مقدار یک مهر زمانی غیرمنفی در مبنای زمانیSystemClock#uptimeMillis()است.
واحدهای اندازهگیری
برای تمام روشهایی که واحد اندازهگیری غیر از زمان را بیان میکنند، پیشوندهای واحد SI با کد CamelCased را ترجیح دهید.
public long[] getFrequenciesKhz();
public float getStreamVolumeDb();
پارامترهای اختیاری را در انتهای overloadها قرار دهید
اگر متدی با پارامترهای اختیاری overload شده دارید، آن پارامترها را در انتها نگه دارید و ترتیب آنها را با سایر پارامترها یکسان نگه دارید:
public int doFoo(boolean flag);
public int doFoo(int id, boolean flag);
public int doFoo(boolean flag);
public int doFoo(boolean flag, int id);
هنگام اضافه کردن overload برای آرگومانهای اختیاری، رفتار متدهای سادهتر باید دقیقاً به همان شکلی باشد که گویی آرگومانهای پیشفرض برای متدهای پیچیدهتر ارائه شده است.
نتیجه: متدها را به جز متدهایی که آرگومانهای اختیاری اضافه میکنند یا در صورتی که متد چندریختی است، آرگومانهای مختلف را میپذیرند، overload نکنید. اگر متد overload شده اساساً کار متفاوتی انجام میدهد، نام جدیدی به آن بدهید.
متدهایی که پارامترهای پیشفرض دارند باید با @JvmOverloads حاشیهنویسی شوند (فقط کاتلین)
متدها و سازندههایی که پارامترهای پیشفرض دارند باید با @JvmOverloads حاشیهنویسی شوند تا سازگاری دودویی حفظ شود.
برای جزئیات بیشتر، به بخش «سربارگذاری توابع برای مقادیر پیشفرض» در راهنمای رسمی تعامل کاتلین-جاوا مراجعه کنید.
class Greeting @JvmOverloads constructor(
loudness: Int = 5
) {
@JvmOverloads
fun sayHello(prefix: String = "Dr.", name: String) = // ...
}
مقادیر پیشفرض پارامترها را حذف نکنید (فقط در کاتلین)
اگر یک متد با پارامتری با مقدار پیشفرض ارسال شده باشد، حذف مقدار پیشفرض یک تغییر مخرب محسوب میشود.
پارامترهای روش که بیشترین تمایز و شناسایی را دارند، باید در ابتدا باشند.
اگر متدی با چندین پارامتر دارید، مرتبطترین آنها را در ابتدا قرار دهید. پارامترهایی که پرچمها و سایر گزینهها را مشخص میکنند، نسبت به پارامترهایی که شیء مورد نظر را توصیف میکنند، اهمیت کمتری دارند. اگر فراخوانی تکمیل وجود دارد، آن را در آخر قرار دهید.
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);
همچنین ببینید: پارامترهای اختیاری را در انتهای overloadها قرار دهید
سازندگان
الگوی Builder برای ایجاد اشیاء پیچیده جاوا توصیه میشود و معمولاً در اندروید برای مواردی استفاده میشود که:
- ویژگیهای شیء حاصل باید تغییرناپذیر باشند
- تعداد زیادی از ویژگیهای مورد نیاز وجود دارد، برای مثال آرگومانهای سازنده زیادی
- در زمان ساخت، رابطه پیچیدهای بین ویژگیها وجود دارد، برای مثال یک مرحله تأیید لازم است. توجه داشته باشید که این سطح از پیچیدگی اغلب نشاندهنده مشکلاتی در قابلیت استفاده از API است.
در نظر بگیرید که آیا به سازنده نیاز دارید یا خیر. سازندگان در سطح API در صورتی مفید هستند که به موارد زیر عادت داشته باشند:
- فقط تعداد کمی از مجموعه بالقوه بزرگی از پارامترهای ایجاد اختیاری را پیکربندی کنید
- پارامترهای ایجاد اختیاری یا الزامی مختلف، گاهی اوقات از انواع مشابه یا منطبق، را پیکربندی کنید، که در غیر این صورت سایتهای فراخوانی میتوانند خواندن را گیجکننده یا نوشتن را مستعد خطا کنند.
- ایجاد یک شیء را به صورت تدریجی پیکربندی کنید، که در آن چندین قطعه کد پیکربندی مختلف ممکن است هر کدام به عنوان جزئیات پیادهسازی، فراخوانیهایی را در سازنده ایجاد کنند.
- با افزودن پارامترهای ایجاد اختیاری اضافی در نسخههای آینده API، به یک نوع اجازه دهید تا رشد کند.
اگر نوعی با سه یا کمتر پارامتر اجباری و بدون پارامتر اختیاری دارید، تقریباً همیشه میتوانید از سازنده صرف نظر کنید و از یک سازنده ساده استفاده کنید.
کلاسهای منبع کاتلین باید سازندههای @JvmOverloads -annotated با آرگومانهای پیشفرض را به Builders ترجیح دهند، اما میتوانند برای بهبود قابلیت استفاده برای کلاینتهای جاوا، در مواردی که قبلاً ذکر شد، Builders را نیز ارائه دهند.
class Tone @JvmOverloads constructor(
val duration: Long = 1000,
val frequency: Int = 2600,
val dtmfConfigs: List<DtmfConfig> = emptyList()
) {
class Builder {
// ...
}
}
کلاسهای سازنده باید سازنده را برگردانند.
کلاسهای سازنده باید با برگرداندن شیء سازنده (مانند this ) از هر متدی به جز build() امکان زنجیرهسازی متدها را فراهم کنند. اشیاء ساخته شده اضافی باید به عنوان آرگومان ارسال شوند -- سازنده یک شیء متفاوت را برنگردانند . برای مثال:
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();
}
}
در موارد نادری که یک کلاس سازنده پایه باید از افزونه پشتیبانی کند، از یک نوع بازگشتی عمومی استفاده کنید:
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);
}
کلاسهای سازنده باید از طریق یک سازنده ایجاد شوند.
برای حفظ ایجاد سازندهی سازگار از طریق سطح API اندروید، تمام سازندگان باید از طریق یک سازنده ایجاد شوند و نه یک روش سازندهی استاتیک. برای APIهای مبتنی بر کاتلین، Builder باید عمومی باشد، حتی اگر انتظار میرود کاربران کاتلین به طور ضمنی از طریق یک روش کارخانهای/مکانیسم ایجاد به سبک DSL به سازنده تکیه کنند. کتابخانهها نباید از @PublishedApi internal برای پنهان کردن انتخابی سازندهی کلاس Builder از کلاینتهای کاتلین استفاده کنند.
public class Tone {
public static Builder builder();
public static class Builder {
}
}
public class Tone {
public static class Builder {
public Builder();
}
}
تمام آرگومانهای سازندهها باید الزامی باشند (مانند @NonNull)
اختیاری، برای مثال @Nullable ، آرگومانها باید به متدهای setter منتقل شوند. سازنده سازنده باید یک NullPointerException صادر کند (استفاده از Objects.requireNonNull را در نظر بگیرید).
کلاسهای سازنده باید کلاسهای داخلی استاتیک نهایی از انواع ساخته شده خود باشند.
برای سازماندهی منطقی درون یک بسته، کلاسهای سازنده معمولاً باید به عنوان کلاسهای داخلی نهایی از انواع ساخته شده خود نمایش داده شوند، به عنوان مثال Tone.Builder به جای ToneBuilder .
سازندگان ممکن است یک سازنده برای ایجاد یک نمونه جدید از یک نمونه موجود داشته باشند
سازندگان میتوانند شامل یک سازنده کپی برای ایجاد یک نمونه سازنده جدید از یک سازنده یا شیء ساخته شده موجود باشند. آنها نباید روشهای جایگزینی برای ایجاد نمونههای سازنده از سازندگان یا اشیاء ساخته شده موجود ارائه دهند.
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);
}
}
اگر سازنده از سازندهی کپی استفاده میکند، تنظیمکنندههای سازنده باید آرگومانهای @Nullable را بپذیرند.
اگر قرار باشد یک نمونه جدید از سازنده از یک نمونه موجود ایجاد شود، تنظیم مجدد ضروری است. اگر هیچ سازنده کپی در دسترس نباشد، سازنده میتواند آرگومانهای @Nullable یا @NonNullable داشته باشد.
public static class Builder {
public Builder(Builder original);
public Builder setObjectValue(@Nullable Object value);
}
تنظیمکنندههای سازنده ممکن است آرگومانهای @Nullable را برای ویژگیهای اختیاری دریافت کنند.
اغلب استفاده از یک مقدار nullable برای ورودی درجه دوم، به خصوص در کاتلین که از آرگومانهای پیشفرض به جای سازندگان و overloadها استفاده میکند، سادهتر است.
علاوه بر این، setter های @Nullable آنها را با getter های آنها مطابقت می دهند، که برای ویژگی های اختیاری باید @Nullable باشند.
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();
}
کاربرد رایج در کاتلین:
fun createValue(optionalValue: OptionalValue? = null) =
Value.Builder()
.apply { optionalValue?.let { setOptionalValue(it) } }
.build()
fun createValue(optionalValue: OptionalValue? = null) =
Value.Builder()
.setOptionalValue(optionalValue)
.build()
مقدار پیشفرض (اگر تنظیمکننده فراخوانی نشده باشد) و معنی null باید به درستی هم در تنظیمکننده و هم در دریافتکننده مستندسازی شود.
/**
* ...
*
* <p>Defaults to {@code null}, which means the optional value won't be used.
*/
میتوان برای ویژگیهای تغییرپذیر، تنظیمکنندههای سازنده را در جایی که تنظیمکنندهها در کلاس ساختهشده موجود هستند، ارائه داد.
اگر کلاس شما ویژگیهای تغییرپذیر دارد و به یک کلاس Builder نیاز دارد، ابتدا از خود بپرسید که آیا کلاس شما واقعاً باید ویژگیهای تغییرپذیر داشته باشد یا خیر.
در مرحله بعد، اگر مطمئن هستید که به ویژگیهای قابل تغییر نیاز دارید، تصمیم بگیرید که کدام یک از سناریوهای زیر برای مورد استفاده مورد انتظار شما بهتر عمل میکند:
شیء ساخته شده باید بلافاصله قابل استفاده باشد، بنابراین باید برای تمام ویژگیهای مربوطه، چه قابل تغییر و چه تغییرناپذیر، تنظیمکنندههایی (setter) ارائه شود.
map.put(key, new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .setUsefulMutableProperty(usefulValue) .build());ممکن است قبل از اینکه شیء ساخته شده بتواند مفید واقع شود، نیاز به انجام برخی فراخوانیهای اضافی باشد، بنابراین نباید برای ویژگیهای قابل تغییر، setter ارائه شود.
Value v = new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .build(); v.setUsefulMutableProperty(usefulValue) Result r = v.performSomeAction(); Key k = callSomeMethod(r); map.put(k, v);
این دو سناریو را با هم قاطی نکنید.
Value v = new Value.Builder(requiredValue)
.setImmutableProperty(immutableValue)
.setUsefulMutableProperty(usefulValue)
.build();
Result r = v.performSomeAction();
Key k = callSomeMethod(r);
map.put(k, v);
سازندگان نباید گیرنده داشته باشند
Getter باید روی شیء ساخته شده باشد، نه روی سازنده.
تنظیمکنندههای سازنده باید دریافتکنندههای متناظری در کلاس ساختهشده داشته باشند.
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();
}
نامگذاری متد سازنده
نام متدهای سازنده باید از سبکهای setFoo() ، addFoo() یا clearFoo() استفاده کند.
انتظار میرود کلاسهای سازنده یک متد build() تعریف کنند.
کلاسهای سازنده باید یک متد build() تعریف کنند که نمونهای از شیء ساخته شده را برمیگرداند.
متدهای build() سازنده باید اشیاء @Null را برگردانند
انتظار میرود متد build() سازنده، یک نمونه غیر تهی از شیء ساخته شده را برگرداند. در صورتی که شیء به دلیل پارامترهای نامعتبر قابل ایجاد نباشد، اعتبارسنجی میتواند به متد build موکول شود و یک IllegalStateException صادر شود.
قفلهای داخلی را در معرض دید قرار ندهید
متدهای موجود در API عمومی نباید از کلمه کلیدی synchronized استفاده کنند. این کلمه کلیدی باعث میشود که شیء یا کلاس شما به عنوان قفل استفاده شود و از آنجایی که در معرض دید دیگران قرار دارد، اگر کد دیگری خارج از کلاس شما شروع به استفاده از آن برای اهداف قفلگذاری کند، ممکن است با عوارض جانبی غیرمنتظرهای مواجه شوید.
در عوض، هرگونه قفلگذاری لازم را روی یک شیء داخلی و خصوصی انجام دهید.
public synchronized void doThing() { ... }
private final Object mThingLock = new Object();
public void doThing() {
synchronized (mThingLock) {
...
}
}
متدهای دارای استایل دسترسی باید از دستورالعملهای ویژگی کاتلین پیروی کنند.
وقتی از منابع کاتلین مشاهده شود، متدهای دارای سبک دسترسی - آنهایی که از پیشوندهای get ، set یا is استفاده میکنند - نیز به عنوان ویژگیهای کاتلین در دسترس خواهند بود. برای مثال، int getField() که در جاوا تعریف شده است، در کاتلین به عنوان ویژگی val field: Int در دسترس است.
به همین دلیل، و برای برآورده کردن انتظارات کلی توسعهدهندگان در مورد رفتار متدهای دسترسی، متدهایی که از پیشوندهای متد دسترسی استفاده میکنند باید رفتاری مشابه فیلدهای جاوا داشته باشند. در موارد زیر از پیشوندهای به سبک دسترسی استفاده نکنید:
- این روش عوارض جانبی دارد -- نام توصیفیتری برای روش ترجیح دهید
- این روش شامل کار محاسباتی پرهزینه است -- ترجیحاً
compute - این روش شامل مسدود کردن یا انجام کارهای طولانی مدت برای بازگرداندن یک مقدار، مانند IPC یا سایر ورودی/خروجیها است -- ترجیحاً
fetch - این متد، نخ را تا زمانی که بتواند مقداری را برگرداند، مسدود میکند -- prefer
await - این متد در هر فراخوانی، یک نمونه شیء جدید برمیگرداند -- prefer
create - ممکن است متد با موفقیت مقداری را برنگرداند --
requestترجیح میدهد
توجه داشته باشید که انجام یک کار محاسباتی سنگین یک بار و ذخیره مقدار برای فراخوانیهای بعدی، همچنان به عنوان انجام کار محاسباتی سنگین محسوب میشود. Jank در فریمهای مختلف مستهلک نمیشود.
از پیشوند is برای متدهای دسترسی بولی استفاده کنید
This is the standard naming convention for boolean methods and fields in Java. Generally, boolean method and variable names should be written as questions that are answered by the return value.
Java boolean accessor methods should follow a set / is naming scheme and fields should prefer is , as in:
// 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;
Using set / is for Java accessor methods or is for Java fields will allow them to be used as properties from Kotlin:
obj.isVisible = true
obj.isFactoryResetProtectionEnabled = false
if (!obj.isAvailable) return
Properties and accessor methods should generally use positive naming, for example Enabled rather than Disabled . Using negative terminology inverts the meaning of true and false and makes it more difficult to reason about behavior.
// Passing false here is a double-negative.
void setFactoryResetProtectionDisabled(boolean disabled);
In cases where the boolean describes inclusion or ownership of a property, you may use has rather than is ; however, this will not work with Kotlin property syntax:
// 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();
Some alternative prefixes that may be more suitable include can and 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();
Methods that toggle behaviors or features may use the is prefix and Enabled suffix:
// "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()
Similarly, methods that indicate the dependency on other behaviors or features may use is prefix and Supported or Required suffix:
// "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()
Generally, method names should be written as questions that are answered by the return value.
Kotlin property methods
For a class property var foo: Foo Kotlin will generate get / set methods using a consistent rule: prepend get and uppercase the first character for the getter, and prepend set and uppercase the first character for the setter. The property declaration will produce methods named public Foo getFoo() and public void setFoo(Foo foo) , respectively.
If the property is of type Boolean an additional rule applies in name generation: if the property name begins with is , then get isn't prepended for the getter method name, the property name itself is used as the getter. Therefore, prefer naming Boolean properties with an is prefix in order to follow the naming guideline:
var isVisible: Boolean
If your property is one of the aforementioned exceptions and begins with an appropriate prefix, use the @get:JvmName annotation on the property to manually specify the appropriate name:
@get:JvmName("hasTransientState")
var hasTransientState: Boolean
@get:JvmName("canRecord")
var canRecord: Boolean
@get:JvmName("shouldFitWidth")
var shouldFitWidth: Boolean
Bitmask accessors
See Use @IntDef for bitmask flags for API guidelines regarding defining bitmask flags.
Setters
Two setter methods should be provided: one that takes a full bitstring and overwrites all existing flags and another that takes a custom bitmask to allow more flexibility.
/**
* 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);
Getters
One getter should be provided to obtain the full bitmask.
/**
* 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();
Use public instead of protected
Always prefer public to protected in public API. Protected access ends up being painful in the long run, because implementers have to override to provide public accessors in cases where external access by default would have been just as good.
Remember that protected visibility doesn't prevent developers from calling an API -- it only makes it slightly more obnoxious.
Implement neither or both of equals() and hashCode()
If you override one, you must override the other.
Implement toString() for data classes
Data classes are encouraged to override toString() , to help developers debug their code.
Document whether the output is for program behavior or debugging
Decide whether you want program behavior to rely on your implementation or not. For example, UUID.toString() and File.toString() document their specific format for programs to use. If you are exposing information for debugging only, like Intent , then imply inherit docs from the superclass.
Don't include extra information
All the information available from toString() should also be available through the public API of the object. Otherwise, you are encouraging developers to parse and rely on your toString() output, which will prevent future changes. A good practice is to implement toString() using only the object's public API.
Discourage reliance on debug output
While it's impossible to prevent developers from depending on debug output, including the System.identityHashCode of your object in its toString() output will make it very unlikely that two different objects will have equal toString() output.
@Override
public String toString() {
return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + " {mFoo=" + mFoo + "}";
}
This can effectively discourage developers from writing test assertions like assertThat(a.toString()).isEqualTo(b.toString()) on your objects.
Use createFoo when returning newly created objects
Use the prefix create , not get or new , for methods that will create return values, for example by constructing new objects.
When the method will create an object to return, make that clear in the method name.
public FooThing getFooThing() {
return new FooThing();
}
public FooThing createFooThing() {
return new FooThing();
}
Methods accepting File objects should also accept streams
Data storage locations on Android aren't always files on disk. For example, content passed across user boundaries is represented as content:// Uri s. To enable processing of various data sources, APIs which accept File objects should also accept InputStream , OutputStream , or both.
public void setDataSource(File file)
public void setDataSource(InputStream stream)
Take and return raw primitives instead of boxed versions
If you need to communicate missing or null values, consider using -1 , Integer.MAX_VALUE , or Integer.MIN_VALUE .
public java.lang.Integer getLength()
public void setLength(java.lang.Integer)
public int getLength()
public void setLength(int value)
Avoiding class equivalents of primitive types avoids the memory overhead of these classes, method access to values, and, more importantly, autoboxing that comes from casting between primitive and object types. Avoiding these behaviors saves on memory and on temporary allocations that can lead to expensive and more frequent garbage collections.
Use annotations to clarify valid parameter and return values
Developer annotations were added to help clarify allowable values in various situations. This makes it easier for tools to help developers when they supply incorrect values (for example, passing an arbitrary int when the framework requires one of a specific set of constant values). Use any and all of the following annotations when appropriate:
Nullability
Explicit nullability annotations are required for Java APIs, but the concept of nullability is part of the Kotlin language and nullability annotations should never be used in Kotlin APIs.
@Nullable : Indicates that a given return value, parameter, or field can be null:
@Nullable
public String getName()
public void setName(@Nullable String name)
@NonNull : Indicates that a given return value, parameter, or field can't be null. Marking things as @Nullable is relatively new to Android, so most of Android's API methods aren't consistently documented. Therefore we have a tri-state of "unknown, @Nullable , @NonNull " which is why @NonNull is part of the API guidelines:
@NonNull
public String getName()
public void setName(@NonNull String name)
For Android platform docs, annotating your method parameters will automatically generate documentation in the form "This value may be null." unless "null" is explicitly used elsewhere in the parameter doc.
Existing "not really nullable" methods: Existing methods in the API without a declared @Nullable annotation may be annotated @Nullable if the method can return null under specific, obvious circumstances (such as findViewById() ). Companion @NotNull requireFoo() methods that throw IllegalArgumentException should be added for developers who don't want to null check.
Interface methods: new APIs should add the proper annotation when implementing interface methods, like Parcelable.writeToParcel() (ie, that method in the implementing class should be writeToParcel(@NonNull Parcel, int) , not writeToParcel(Parcel, int) ); existing APIs that are lacking the annotations don't need to be "fixed", though.
Nullability enforcement
In Java, methods are recommended to perform input validation for @NonNull parameters using Objects.requireNonNull() and throw a NullPointerException when the parameters are null. This is automatically performed in Kotlin.
منابع
Resource identifiers: Integer parameters that denote ids for specific resources should be annotated with the appropriate resource-type definition. There is an annotation for every type of resource, such as @StringRes , @ColorRes , and @AnimRes , in addition to the catch-all @AnyRes . For example:
public void setTitle(@StringRes int resId)
@IntDef for constant sets
Magic constants: String and int parameters that are meant to receive one of a finite set of possible values denoted by public constants should be annotated appropriately with @StringDef or @IntDef . These annotations allow you to create a new annotation that you can use that works like a typedef for allowable parameters. For example:
/** @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);
Methods are recommended to check the validity of the annotated parameters and throw an IllegalArgumentException if the parameter isn't part of the @IntDef
@IntDef for bitmask flags
The annotation can also specify that the constants are flags, and can be combined with & and 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 for string constant sets
There is also the @StringDef annotation, which is exactly like @IntDef in the previous section, but for String constants. You can include multiple "prefix" values which are used to automatically emit documentation for all values.
@SdkConstant for SDK constants
@SdkConstant Annotate public fields when they are one of these SdkConstant values: 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";
Provide compatible nullability for overrides
For API compatibility, the nullability of overrides should be compatible with the current nullability of the parent. The following table represents the compatibility expectations. Plainly, overrides should only be as restrictive or more restrictive than the element they override.
| نوع | Parent | کودک |
|---|---|---|
| Return type | Unannotated | Unannotated or nonnull |
| Return type | Nullable | Nullable or nonnull |
| Return type | Nonnull | Nonnull |
| Fun argument | Unannotated | Unannotated or nullable |
| Fun argument | Nullable | Nullable |
| Fun argument | Nonnull | Nullable or nonnull |
Prefer non-nullable (such as @NonNull) arguments where possible
When methods are overloaded, prefer that all arguments are nonnull.
public void startActivity(@NonNull Component component) { ... }
public void startActivity(@NonNull Component component, @NonNull Bundle options) { ... }
This rule applies to overloaded property setters as well. The primary argument should be nonnull and clearing the property should be implemented as a separate method. This prevents "nonsense" calls where the developer must set trailing parameters even though they aren't required.
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()
Prefer non-nullable (such as @NonNull) return types for containers
For container types such as Bundle or Collection , return an empty -- and immutable, where applicable -- container. In cases where null would be used to distinguish availability of a container, consider providing a separate boolean method.
@NonNull
public Bundle getExtras() { ... }
Nullability annotations for get and set pairs must agree
Get and set method pairs for a single logical property should always agree in their nullability annotations. Failing to follow this guideline will defeat Kotlin's property syntax, and adding disagreeing nullability annotations to existing property methods is therefore a source-breaking change for Kotlin users.
@NonNull
public Bundle getExtras() { ... }
public void setExtras(@NonNull Bundle bundle) { ... }
Return value in failure or error conditions
All APIs should permit apps to react to errors. Returning false , -1 , null , or other catch-all values of "something went wrong" don't tell a developer enough about the failure to set user expectations or accurately track reliability of their app in the field. When designing an API, imagine that you are building an app. If you encounter an error, does the API give you enough information to present it to the user or react appropriately?
- It's fine (and encouraged) to include detailed information in an exception message, but developers shouldn't have to parse it to handle the error appropriately. Verbose error codes or other information should be exposed as methods.
- Make sure your chosen error handling option gives you the flexibility to introduce new error types in the future. For
@IntDef, that means including anOTHERorUNKNOWNvalue - when returning a new code, you can check the caller'stargetSdkVersionto avoid returning an error code the app doesn't know about. For exceptions, have a common superclass that your exceptions implement, so that any code that handles that type will also catch and handle subtypes. - It should be difficult or impossible for a developer to accidentally ignore an error -- if your error is communicated by returning a value, annotate your method with
@CheckResult.
Prefer throwing a ? extends RuntimeException when a failure or error condition is reached due to something that the developer did wrong, for example ignoring constraints on input parameters or failing to check observable state.
Setter or action (for example, perform ) methods may return an integer status code if the action may fail as a result of asynchronously-updated state or conditions outside the developer's control.
Status codes should be defined on the containing class as public static final fields, prefixed with ERROR_ , and enumerated in an @hide @IntDef annotation.
Method names should always begin with the verb, not the subject
The name of the method should always begin with the verb (such as get , create , reload , etc.), not the object you're acting on.
public void tableReload() {
mTable.reload();
}
public void reloadTable() {
mTable.reload();
}
Prefer Collection types over arrays as return or parameter type
Generically typed collection interfaces provide several advantages over arrays, including stronger API contracts around uniqueness and ordering, support for generics, and a number of developer-friendly convenience methods.
Exception for primitives
If the elements are primitives, do prefer arrays instead, in order to avoid the cost of auto-boxing. See Take and return raw primitives instead of boxed versions
Exception for performance-sensitive code
In certain scenarios, where the API is used in performance-sensitive code (like graphics or other measure/layout/draw APIs), it is acceptable to use arrays instead of collections in order to reduce allocations and memory churn.
Exception for Kotlin
Kotlin arrays are invariant and the Kotlin language provides ample utility APIs around arrays, so arrays are on-par with List and Collection for Kotlin APIs intended to be accessed from Kotlin.
Prefer @NonNull collections
Always prefer @NonNull for collection objects. When returning an empty collection, use the appropriate Collections.empty method to return a low cost, correctly typed, and immutable collection object.
Where type annotations are supported, always prefer @NonNull for collection elements.
You should also prefer @NonNull when using arrays instead of collections (see previous item ). If object allocation is a concern, create a constant and pass it along - after all, an empty array is immutable. Example:
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;
}
Collection mutability
Kotlin APIs should prefer read-only (not Mutable ) return types for collections by default unless the API contract specifically requires a mutable return type.
Java APIs, however, should prefer mutable return types by default because the Android platform implementation of Java APIs doesn't yet provide a convenient implementation of immutable collections. The exception to this rule is Collections.empty return types, which are immutable. In cases where mutability could be exploited by clients -- on purpose or by mistake -- to break the API's intended usage pattern, Java APIs should strongly consider returning a shallow copy of the collection.
@Nullable
public PermissionInfo[] getGrantedPermissions() {
return mPermissions;
}
@NonNull
public Set<PermissionInfo> getGrantedPermissions() {
if (mPermissions == null) {
return Collections.emptySet();
}
return new ArraySet<>(mPermissions);
}
Explicitly mutable return types
APIs that return collections should ideally not modify the returned collection object after returning. If the returned collection must change or be reused in some way -- for example, an adapted view of a mutable dataset -- the precise behavior of when the contents can change must be explicitly documented or follow established API naming conventions.
/**
* Returns a view of this object as a list of [Item]s.
*/
fun MyObject.asList(): List<Item> = MyObjectListWrapper(this)
The Kotlin .asFoo() convention is described below and permits the collection returned by .asList() to change if the original collection changes.
Mutability of returned data-type objects
Similar to APIs that return collections, APIs that return data-type objects should ideally not modify the properties of the returned object after returning.
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)
}
In extremely limited cases, some performance-sensitive code may benefit from object pooling or reuse. Don't write your own object pool data structure and don't expose reused objects in public APIs. In either case, be extremely careful about managing concurrent access.
Use of vararg parameter type
Both Kotlin and Java APIs are encouraged to use vararg in cases where the developer would be likely to create an array at the call site for the sole purpose of passing multiple, related parameters of the same type.
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);
Defensive copies
Both Java and Kotlin implementations of vararg parameters compile to the same array-backed bytecode and as a result may be called from Java code with a mutable array. API designers are strongly encouraged to create a defensive shallow copy of the array parameter in cases where it will be persisted to a field or anonymous inner class.
public void setValues(SomeObject... values) {
this.values = Arrays.copyOf(values, values.length);
}
Note that creating a defensive copy doesn't provide any protection against concurrent modification between the initial method call and the creation of the copy, nor does it protect against mutation of the objects contained in the array.
Provide correct semantics with collection type parameters or returned types
List<Foo> is default option, but consider other types to provide additional meaning:
Use
Set<Foo>, if your API is indifferent to the order of elements and it doesn't allow duplicates or duplicates are meaningless.Collection<Foo>,if your API is indifferent to the order and allows duplicates.
Kotlin conversion functions
Kotlin frequently uses .toFoo() and .asFoo() to obtain an object of a different type from an existing object where Foo is the name of the conversion's return type. This is consistent with the familiar JDK Object.toString() . Kotlin takes this further by using it for primitive conversions such as 25.toFloat() .
The distinction between conversions named .toFoo() and .asFoo() is significant:
Use .toFoo() when creating a new, independent object
Like .toString() , a "to" conversion returns a new, independent object. If the original object is modified later, the new object won't reflect those changes. Similarly, if the new object is modified later, the old object won't reflect those changes.
fun Foo.toBundle(): Bundle = Bundle().apply {
putInt(FOO_VALUE_KEY, value)
}
Use .asFoo() when creating a dependent wrapper, decorated object, or cast
Casting in Kotlin is performed using the as keyword. It reflects a change in interface but not a change in identity. When used as a prefix in an extension function, .asFoo() decorates the receiver. A mutation in the original receiver object will be reflected in the object returned by asFoo() . A mutation in the new Foo object may be reflected in the original object.
fun <T> Flow<T>.asLiveData(): LiveData<T> = liveData {
collect {
emit(it)
}
}
Conversion functions should be written as extension functions
Writing conversion functions outside of both the receiver and the result class definitions reduces coupling between types. An ideal conversion needs only public API access to the original object. This proves by example that a developer can write analogous conversions to their own preferred types as well.
Throw appropriate specific exceptions
Methods must not throw generic exceptions such as java.lang.Exception or java.lang.Throwable , instead an appropriate specific exception has to be used like java.lang.NullPointerException to allow developers to handle exceptions without being overly broad.
Errors that are unrelated to the arguments provided directly to the publicly invoked method should throw java.lang.IllegalStateException instead of java.lang.IllegalArgumentException or java.lang.NullPointerException .
Listeners and callbacks
These are the rules around the classes and methods used for listener and callback mechanisms.
Callback class names should be singular
Use MyObjectCallback instead of MyObjectCallbacks .
Callback method names should be of the format on
onFooEvent signifies that FooEvent is happening and that the callback should act in response.
Past versus present tense should describe timing behavior
Callback methods regarding events should be named to indicate whether the event has already happened or is in the process of happening.
For example, if the method is called after a click action has been performed:
public void onClicked()
However, if the method is responsible for performing the click action:
public boolean onClick()
Callback registration
When a listener or callback can be added or removed from an object, the associated methods should be named add and remove or register and unregister. Be consistent with the existing convention used by the class or by other classes in the same package. When no such precedent exists, prefer add and remove.
Methods involving registering or unregistering callbacks should specify the whole name of the callback type.
public void addFooCallback(@NonNull FooCallback callback);
public void removeFooCallback(@NonNull FooCallback callback);
public void registerFooCallback(@NonNull FooCallback callback);
public void unregisterFooCallback(@NonNull FooCallback callback);
Avoid getters for callbacks
Don't add getFooCallback() methods. This is a tempting escape hatch for cases where developers may want to chain an existing callback together with their own replacement, but it is brittle and makes the current state difficult to reason about for component developers. For example,
- Developer A calls
setFooCallback(a) - Developer B calls
setFooCallback(new B(getFooCallback())) - Developer A wishes to remove its callback
aand has no way to do so without knowledge ofB's type, andBhaving been built to allow such modifications of its wrapped callback.
Accept Executor to control callback dispatch
When registering callbacks that have no explicit threading expectations (pretty much anywhere outside the UI toolkit), it is strongly encouraged to include an Executor parameter as part of registration to allow the developer to specify the thread upon which the callbacks will be invoked.
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
As an exception to our usual guidelines about optional parameters , it is acceptable to provide an overload omitting the Executor even though it isn't the final argument in the parameter list. If the Executor isn't provided, the callback should be invoked on the main thread using Looper.getMainLooper() and this should be documented on the associated overloaded method.
/**
* ...
* 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)
Executor implementation gotchas: Note that the following is a valid executor!
public class SynchronousExecutor implements Executor {
@Override
public void execute(Runnable r) {
r.run();
}
}
This means that when implementing APIs that take this form, your incoming binder object implementation on the app process side must call Binder.clearCallingIdentity() before invoking the app's callback on the app-supplied Executor . This way any app code that uses binder identity (such as Binder.getCallingUid() ) for permission checks correctly attributes the code running to the app and not to the system process calling into the app. If users of your API want the UID or PID information of the caller then this should be an explicit part of your API surface, rather than implicit based on where the Executor they supplied ran.
Specifying an Executor should be supported by your API. In performance-critical cases apps may need to run code either immediately or synchronously with feedback from your API. Accepting an Executor permits this. Defensively creating an additional HandlerThread or similar to trampoline from defeats this desirable use case.
If an app is going to run expensive code somewhere in their own process, let them . The workarounds that app developers will find to overcome your restrictions will be much harder to support in the long term.
Exception for single callback: when the nature of the events being reported calls for only supporting a single callback instance, use the following style:
public void setFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
public void clearFooCallback()
Use Executor instead of Handler
Android's Handler was used as a standard for redirecting callback execution to a specific Looper thread in the past. This standard was changed to prefer Executor as most app developers manage their own thread pools, making the main or UI thread the only Looper thread available to the app. Use Executor to give developers the control they need to reuse their existing/preferred execution contexts.
Modern concurrency libraries like kotlinx.coroutines or RxJava provide their own scheduling mechanisms that perform their own dispatch when needed, which makes it important to provide the ability to use a direct executor (such as Runnable::run ) to avoid latency from double thread hops. For example, one hop to post to a Looper thread using a Handler followed by another hop from the app's concurrency framework.
Exceptions to this guideline are rare. Common appeals for an exception include:
I have to use a Looper because I need a Looper to epoll for the event. This exception request is granted as the benefits of Executor can't be realized in this situation.
I don't want app code to block my thread publishing the event. This exception request is typically not granted for code that runs in an app process. Apps that get this wrong are only hurting themselves, not impacting overall system health. Apps that get it right or use a common concurrency framework shouldn't pay additional latency penalties.
Handler is locally consistent with other similar APIs in the same class. This exception request is granted situationally. Preference is for Executor -based overloads to be added, migrating Handler implementations to use the new Executor implementation. ( myHandler::post is a valid Executor !) Depending on the size of the class, number of existing Handler methods, and likelihood that developers would need to use existing Handler based methods alongside the new method, an exception may be granted to add a new Handler -based method.
Symmetry in registration
If there is a way to add or register something, there should also be a way to remove/unregister it. The method
registerThing(Thing)
should have a matching
unregisterThing(Thing)
Provide a request identifier
If it is reasonable for a developer to reuse a callback, provide an identifier object to tie the callback to the request.
class RequestParameters {
public int getId() { ... }
}
class RequestExecutor {
public void executeRequest(
RequestParameters parameters,
Consumer<RequestParameters> onRequestCompletedListener) { ... }
}
Multiple-method callback objects
Multiple-method callbacks should prefer interface and use default methods when adding to previously-released interfaces. Previously, this guideline recommended abstract class due to the lack of default methods in Java 7.
public interface MostlyOptionalCallback {
void onImportantAction();
default void onOptionalInformation() {
// Empty stub, this method is optional.
}
}
Use android.os.OutcomeReceiver when modeling a nonblocking function call
OutcomeReceiver<R,E> reports a result value R when successful or E : Throwable otherwise - the same things a plain method call can do. Use OutcomeReceiver as the callback type when converting a blocking method that returns a result or throws an exception to a nonblocking async method:
interface FooType {
// Before:
public FooResult requestFoo(FooRequest request);
// After:
public void requestFooAsync(FooRequest request, Executor executor,
OutcomeReceiver<FooResult, Throwable> callback);
}
Async methods converted in this way always return void . Any result that requestFoo would return is instead reported to requestFooAsync 's callback parameter's OutcomeReceiver.onResult by calling it on the provided executor . Any exception that requestFoo would throw is instead reported to the OutcomeReceiver.onError method in the same way.
Using OutcomeReceiver for reporting async method results also affords a Kotlin suspend fun wrapper for async methods using the Continuation.asOutcomeReceiver extension from androidx.core:core-ktx :
suspend fun FooType.requestFoo(request: FooRequest): FooResult =
suspendCancellableCoroutine { continuation ->
requestFooAsync(request, Runnable::run, continuation.asOutcomeReceiver())
}
Extensions like this enable Kotlin clients to call nonblocking async methods with the convenience of a plain function call without blocking the calling thread. These 1-1 extensions for platform APIs may be offered as part of the androidx.core:core-ktx artifact in Jetpack when combined with standard version compatibility checks and considerations. See the documentation for asOutcomeReceiver for more information, cancellation considerations and samples.
Async methods that don't match the semantics of a method returning a result or throwing an exception when its work is complete shouldn't use OutcomeReceiver as a callback type. Instead consider one of the other options listed in the following section.
Prefer functional interfaces over creating new single abstract method (SAM) types
API level 24 added the java.util.function.* ( reference docs ) types, which offer generic SAM interfaces such as Consumer<T> that are suitable for use as callback lambdas. In many cases, creating new SAM interfaces provides little value in terms of type safety or communicating intent while unnecessarily expanding the Android API surface area.
Consider using these generic interfaces, rather than creating new ones:
-
Runnable:() -> Unit -
Supplier<R>:() -> R -
Consumer<T>:(T) -> Unit -
Function<T,R>:(T) -> R -
Predicate<T>:(T) -> Boolean - many more available in reference docs
Placement of SAM parameters
SAM parameters should be placed last to enable idiomatic usage from Kotlin, even if the method is being overloaded with additional parameters.
public void schedule(Runnable runnable)
public void schedule(int delay, Runnable runnable)
اسناد
These are rules about the public docs (Javadoc) for APIs.
All public APIs must be documented
All public APIs must have sufficient documentation to explain how a developer would use the API. Assume the developer found the method using autocomplete or while browsing through API reference docs and has a minimal amount of context from the adjacent API surface (for example, the same class).
Methods
Method parameters and return values must be documented using @param and @return docs annotations, respectively. Format the Javadoc body as though it's preceded by "This method...".
In cases where a method takes no parameters, has no special considerations, and returns what the method name says it does, you can omit the @return and write docs similar to:
/**
* Returns the priority of the thread.
*/
@IntRange(from = 1, to = 10)
public int getPriority() { ... }
Always use links in Javadoc
Docs should link to other docs for related constants, methods, and other elements. Use Javadoc tags (for example, @see and {@link foo} ), not just plain-text words.
For the following source example:
public static final int FOO = 0;
public static final int BAR = 1;
Don't use raw text or code font:
/**
* 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) { ... }
Instead, use links:
/**
* Sets value to one of {@link #FOO} or {@link #BAR}.
*
* @param value the value being set
*/
public void setValue(@ValueType int value) { ... }
Note that using an IntDef annotation such as @ValueType on a parameter automatically generates documentation specifying the allowed types. See the guidance on annotations for more information on IntDef .
Run update-api or docs target when adding Javadoc
This rule is particularly important when adding @link or @see tags, and make sure the output looks as expected. ERROR output in Javadoc is often due to bad links. Either the update-api or docs Make target performs this check, but the docs target might be quicker if you're only changing Javadoc and don't otherwise need to run the update-api target.
Use {@code foo} to distinguish Java values
Wrap Java values like true , false , and null with {@code...} to distinguish them from documentation text.
When writing documentation in Kotlin sources, you can wrap code with backticks like you would for Markdown.
@param and @return summaries should be a single sentence fragment
Parameter and return value summaries should start with a lowercase character and contain only a single sentence fragment. If you have additional information that extends beyond a single sentence, move it to the method Javadoc body:
/**
* @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.
*/
Should be changed to:
/**
* @param e element to be appended to this list, must be non-{@code null}
* @return {@code true} on success, {@code false} otherwise
*/
Docs annotations need explanations
Document why annotations @hide and @removed are hidden from the public API. Include instructions for how to replace API elements marked with the @deprecated annotation.
Use @throws to document exceptions
If a method throws a checked exception, for example IOException , document the exception with @throws . For Kotlin-sourced APIs intended for use by Java clients, annotate functions with @Throws .
If a method throws an unchecked exception indicating a preventable error, for example IllegalArgumentException or IllegalStateException , document the exception with an explanation of why the exception is thrown. The thrown exception should also indicate why it was thrown.
Certain cases of unchecked exception are considered implicit and don't need to be documented, such as NullPointerException or IllegalArgumentException where an argument doesn't match an @IntDef or similar annotation that embeds the API contract into the method signature:
/**
* ...
* @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.");
}
// ...
Or, in 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")
}
}
// ...
If the method invokes asynchronous code that might throw exceptions, consider how the developer finds out about and responds to such exceptions. Typically this involves forwarding the exception to a callback and documenting the exceptions thrown on the method that receives them. Asynchronous exceptions shouldn't be documented with @throws unless they're actually rethrown from the annotated method.
End the first sentence of docs with a period
The Doclava tool parses docs simplistically, ending the synopsis doc (the first sentence, used in the quick description at the top of the class docs) as soon as it sees a period (.) followed by a space. This causes two problems:
- If a short doc doesn't end with a period, and if that member has inherited docs that are picked up by the tool, then the synopsis also picks up those inherited docs. For example, see
actionBarTabStylein theR.attrdocs , which has the description of the dimension added into the synopsis. - Avoid "eg" in the first sentence for the same reason, because Doclava ends the synopsis docs after "g.". For example, see
TEXT_ALIGNMENT_CENTERinView.java. Note that Metalava automatically corrects this error by inserting a nonbreaking space after the period; however, don't make this mistake in the first place.
Format docs to be rendered in HTML
Javadoc is rendered in HTML, so format these docs accordingly:
Line breaks should use an explicit
<p>tag. Don't add a closing</p>tag.Don't use ASCII to render lists or tables.
Lists should use
<ul>or<ol>for unordered and ordered, respectively. Each item should begin with an<li>tag, but doesn't need a closing</li>tag. A closing</ul>or</ol>tag is required after the last item.Tables should use
<table>,<tr>for rows,<th>for headers, and<td>for cells. All table tags require matching closing tags. You can useclass="deprecated"on any tag to denote deprecation.To create inline code font, use
{@code foo}.To create code blocks, use
<pre>.All text inside a
<pre>block is parsed by the browser, so be careful with brackets<>. You can escape them with<and>HTML entities.Alternatively, you can leave raw brackets
<>in your code snippet if you wrap the offending sections in{@code foo}. For example:<pre>{@code <manifest>}</pre>
Follow the API reference style guide
To provide consistency in the style for class summaries, method descriptions, parameter descriptions, and other items, follow the recommendations in the official Java language guidelines at How to Write Doc Comments for the Javadoc Tool .
Android Framework-specific rules
These rules are about APIs, patterns, and data structures that are specific to APIs and behaviors built into the Android framework (for example, Bundle or Parcelable ).
Intent builders should use the create*Intent() pattern
Creators for intents should use methods named createFooIntent() .
Use Bundle instead of creating new general-purpose data structures
Avoid creating new general-purpose data structures to represent arbitrary key to typed value mappings. Instead, consider using Bundle .
This typically comes up when writing platform APIs that serve as communication channels between nonplatform apps and services, where the platform doesn't read the data sent across the channel and the API contract may be partially defined outside of the platform (for example, in a Jetpack library).
In cases where the platform does read the data, avoid using Bundle and prefer a strongly typed data class.
Parcelable implementations must have public CREATOR field
Parcelable inflation is exposed through CREATOR , not raw constructors. If a class implements Parcelable , then its CREATOR field must also be a public API and the class constructor taking a Parcel argument must be private.
Use CharSequence for UI strings
When a string is presented in a user interface, use CharSequence to allow for Spannable instances.
If it's just a key or some other label or value that isn't visible to users, String is fine.
Avoid using Enums
IntDef must be used over enums in all platform APIs, and should be strongly considered in unbundled, library APIs. Use enums only when you're certain that new values won't be added.
Benefits of IntDef :
- Enables adding values over time
- Kotlin
whenstatements can fail at runtime if they become no-longer-exhaustive due to an added enum value in platform.
- Kotlin
- No classes or objects used at runtime, only primitives
- While R8 or minfication can avoid this cost for unbundled library APIs, this optimization can't affect platform API classes.
Benefits of Enum
- Idiomatic language feature of Java, Kotlin
- Enables exhaustive switch,
whenstatement usage- Note - values must not change over time, see previous list
- Clearly scoped, and discoverable naming
- Enables compile time verification
- For example, a
whenstatement in Kotlin that returns a value
- For example, a
- Is a functioning class that can implement interfaces, have static helpers, expose member or extension methods, and expose fields.
Follow Android package layering hierarchy
The android.* package hierarchy has an implicit ordering, where lower-level packages can't depend on higher-level packages.
Avoid referring to Google, other companies, and their products
The Android platform is an open-source project and aims to be vendor neutral. The API should be generic and equally usable by system integrators or apps with the requisite permissions.
Parcelable implementations should be final
Parcelable classes defined by the platform are always loaded from framework.jar , so it is invalid for an app to try overriding a Parcelable implementation.
If the sending app extends a Parcelable , the receiving app won't have the sender's custom implementation to unpack with. Note about backward compatibility: if your class historically wasn't final, but didn't have a publicly available constructor, you still can mark it final .
Methods calling into system process should rethrow RemoteException as RuntimeException
RemoteException is typically thrown by internal AIDL, and indicates that the system process has died, or the app is trying to send too much data. In both cases, public API should rethrow as a RuntimeException to prevent apps from persisting security or policy decisions.
If you know the other side of a Binder call is the system process, this boilerplate code is the best-practice:
try {
...
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
Throw specific exceptions for API changes
Public API behaviors might change across API levels and cause app crashes (for instance to enforce new security policies).
When the API needs to throw for a request that was previously valid, throw a new specific exception instead of a generic one. For example, ExportedFlagRequired instead of SecurityException (and ExportedFlagRequired can extend SecurityException ).
This will help app developers and tools detect API behavior changes.
Implement copy constructor instead of clone
Use of the Java clone() method is strongly discouraged due to the lack of API contracts provided by the Object class and difficulties inherent in extending classes that use clone() . Instead, use a copy constructor that takes an object of the same type.
/**
* Constructs a shallow copy of {@code other}.
*/
public Foo(Foo other)
Classes that rely on a Builder for construction should consider adding a Builder copy constructor to allow modifications to the copy.
public class Foo {
public static final class Builder {
/**
* Constructs a Foo builder using data from {@code other}.
*/
public Builder(Foo other)
Use ParcelFileDescriptor over FileDescriptor
The java.io.FileDescriptor object has a poor definition of ownership, which can result in obscure use-after-close bugs. Instead, APIs should return or accept ParcelFileDescriptor instances. Legacy code can convert between PFD and FD if needed using dup() or getFileDescriptor() .
Avoid using odd-sized numerical values
Avoid using short or byte values directly, because they often limit how you might be able to evolve the API in the future.
Avoid using BitSet
java.util.BitSet is great for implementation but not for public API. It's mutable, requires an allocation for high-frequency method calls, and doesn't provide semantic meaning for what each bit represents.
For high-performance scenarios, use an int or long with @IntDef . For low-performance scenarios, consider a Set<EnumType> . For raw binary data, use byte[] .
Prefer android.net.Uri
android.net.Uri is the preferred encapsulation for URIs in Android APIs.
Avoid java.net.URI , because it is overly strict in parsing URIs, and never use java.net.URL , because its definition of equality is severely broken.
Hide annotations marked as @IntDef, @LongDef, or @StringDef
Annotations marked as @IntDef , @LongDef , or @StringDef denote a set of valid constants that can be passed to an API. However, when they are exported as APIs themselves, the compiler inlines the constants and only the (now useless) values remain in the annotation's API stub (for the platform) or JAR (for libraries).
As such, usages of these annotations must be marked with the @hide docs annotation in the platform or @RestrictTo.Scope.LIBRARY) code annotation in libraries. They must be marked @Retention(RetentionPolicy.SOURCE) in both cases to prevent them from appearing in API stubs or JARs.
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STREAM_TYPE_FULL_IMAGE_DATA,
STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}
When building the platform SDK and library AARs, a tool extracts the annotations and bundles them separately from the compiled sources. Android Studio reads this bundled format and enforces the type definitions.
Don't add new setting provider keys
Don't expose new keys from Settings.Global , Settings.System , or Settings.Secure .
Instead, add a proper getter and setter Java API in a relevant class, which is typically a "manager" class. Add a listener mechanism or a broadcast to notify clients of changes as needed.
SettingsProvider settings have a number of problems compared to getters/setters:
- No type safety.
- No unified way to provide a default value.
- No proper way to customize permissions.
- For example, it's not possible to protect your setting with a custom permission.
- No proper way to add custom logic properly.
- For example, it's not possible to change setting A's value depending on setting B's value.
Example: Settings.Secure.LOCATION_MODE has existed for a long time, but the location team has deprecated it for a proper Java API LocationManager.isLocationEnabled() and the MODE_CHANGED_ACTION broadcast, which gave the team a lot more flexibility, and the semantics of the APIs are a lot clearer now.
Don't extend Activity and AsyncTask
AsyncTask is an implementation detail. Instead, expose a listener or, in androidx, a ListenableFuture API instead.
Activity subclasses are impossible to compose. Extending activity for your feature makes it incompatible with other features that require users to do the same. Instead, rely on composition by using tools such as LifecycleObserver .
Use the Context's getUser()
Classes bound to a Context , such as anything returned from Context.getSystemService() should use the user bound to the Context instead of exposing members that target specific users.
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);
}
}
Exception: A method may accept a user argument if it accepts values that don't represent a single user, such as UserHandle.ALL .
Use UserHandle instead of plain ints
UserHandle is preferred to provide type safety and avoid conflating user IDs with uids.
Foobar getFoobarForUser(UserHandle user);
Foobar getFoobarForUser(int userId);
Where unavoidable, an int representing a user ID must be annotated with @UserIdInt .
Foobar getFoobarForUser(@UserIdInt int user);
Prefer listeners or callbacks to broadcast intents
Broadcast intents are very powerful, but they've resulted in emergent behaviors that can negatively impact system health, and so new broadcast intents should be added judiciously.
Here are some specific concerns which result in us discouraging the introduction of new broadcast intents:
When sending broadcasts without the
FLAG_RECEIVER_REGISTERED_ONLYflag, they force-start any apps that aren't already running. While this can sometimes be an intended outcome, it can result in stampeding of dozens of apps, negatively impacting system health. We'd recommend using alternative strategies, such asJobScheduler, to better coordinate when various preconditions are met.When sending broadcasts, there is little ability to filter or adjust the content delivered to apps. This makes it difficult or impossible to respond to future privacy concerns, or introduce behavior changes based on the target SDK of the receiving app.
Since broadcast queues are a shared resource, they can become overloaded and may not result in timely delivery of your event. We've observed several broadcast queues in the wild which have an end-to-end latency of 10 minutes or longer.
For these reasons, we encourage new features to consider using listeners or callbacks or other facilities such as JobScheduler instead of broadcast intents.
In cases where broadcast intents still remain the ideal design, here are some best-practices that should be considered:
- If possible, use
Intent.FLAG_RECEIVER_REGISTERED_ONLYto limit your broadcast to apps that are already running. For example,ACTION_SCREEN_ONuses this design to avoid waking up apps. - If possible, use
Intent.setPackage()orIntent.setComponent()to target the broadcast at a specific app of interest. For example,ACTION_MEDIA_BUTTONuses this design to focus on the current app handling playback controls. - If possible, define your broadcast as a
<protected-broadcast>to prevent malicious apps from impersonating the OS.
Intents in system-bound developer services
Services that are intended to be extended by the developer and bound by the system, for example abstract services like NotificationListenerService , may respond to an Intent action from the system. Such services should meet the following criteria:
- Define a
SERVICE_INTERFACEstring constant on the class containing the fully-qualified class name of the service. This constant must be annotated with@SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION). - Document on the class that a developer must add an
<intent-filter>to theirAndroidManifest.xmlin order to receive Intents from the platform. - Strongly consider adding a system-level permission to prevent rogue apps from sending
Intents to developer services.
Kotlin-Java interop
See the official Android Kotlin-Java interop guide for a full list of guidelines. Select guidelines have been copied to this guide to improve discoverability.
API visibility
Some Kotlin APIs, like suspend fun s, aren't intended to be used by Java developers; however, don't attempt to control language-specific visibility using @JvmSynthetic as it has side-effects on how the API is presented in debuggers that make debugging more difficult.
See the Kotlin-Java interop guide or Async guide for specific guidance.
Companion objects
Kotlin uses companion object to expose static members. In some cases, these will show up from Java on an inner class named Companion rather than on the containing class. Companion classes may show as empty classes in API text files -- that is working as intended.
To maximize compatibility with Java, annotate companion objects' non-constant fields with @JvmField and public functions with @JvmStatic to expose them directly on the containing class.
companion object {
@JvmField val BIG_INTEGER_ONE = BigInteger.ONE
@JvmStatic fun fromPointF(pointf: PointF) {
/* ... */
}
}
Evolution of Android platform APIs
This section describes policies regarding what types of changes you can make to existing Android APIs and how you should implement those changes to maximize compatibility with existing apps and codebases.
Binary-breaking changes
Avoid binary-breaking changes in finalized public API surfaces. These types of changes generally raise errors when running make update-api , but there might be edge cases that Metalava's API check doesn't catch. When in doubt, refer to the Eclipse Foundation's Evolving Java-based APIs guide for a detailed explanation of what types of API changes are compatible in Java. Binary-breaking changes in hidden (for example, system) APIs should follow the deprecate/replace cycle.
Source-breaking changes
We discourage source-breaking changes even if they aren't binary breaking. One example of a binary compatible but source-breaking change is adding a generic to an existing class, which is binary compatible but can introduce compilation errors due to inheritance or ambiguous references. Source-breaking changes won't raise errors when running make update-api , so you must take care to understand the impact of changes to existing API signatures.
In some cases, source-breaking changes become necessary to improve the developer experience or code correctness. For example, adding nullability annotations to Java sources improves interoperability with Kotlin code and reduces the likelihood of errors, but often requires changes -- sometimes significant changes -- to source code.
Changes to private APIs
You can change APIs annotated with @TestApi at any time.
You must preserve APIs annotated with @SystemApi for three years. You must remove or refactor a system API on the following schedule:
- API y - Added
- API y+1 - Deprecation
- Mark the code with
@Deprecated. - Add replacements, and link to the replacement in the Javadoc for the deprecated code using the
@deprecateddocs annotation. - During the development cycle, file bugs against internal users telling them the API is being deprecated. This helps validate that the replacement APIs are adequate.
- Mark the code with
- API y+2 - Soft removal
- Mark the code with
@removed. - Optionally, throw or no-op for apps that target the current SDK level for the release.
- Mark the code with
- API y+3 - Hard removal
- Completely remove the code from the source tree.
منسوخ شدن
We consider deprecation an API change, and it can occur in a major (such as letter) release. Use the @Deprecated source annotation and @deprecated <summary> docs annotation together when deprecating APIs. Your summary must include a migration strategy. This strategy might link to a replacement API or explain why you shouldn't use the 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)
You must also deprecate APIs defined in XML and exposed in Java, including attributes and styleable properties exposed in the android.R class, with a summary:
<!-- Attribute whether the accessibility service ...
{@deprecated Not used by the framework}
-->
<attr name="canRequestEnhancedWebAccessibility" format="boolean" />
When to deprecate an API
Deprecations are most useful for discouraging adoption of an API in new code .
We also require that you mark APIs as @deprecated before they're @removed , but this doesn't provide strong motivation for developers to migrate away from an API they're already using.
Before deprecating an API, consider the impact on developers. The effects of deprecating an API include:
-
javacemits a warning during compilation.- Deprecation warnings can't be suppressed globally or baselined, so developers using
-Werrorneed to individually fix or suppress every usage of a deprecated API before they can update their compile SDK version. - Deprecation warnings on imports of deprecated classes can't be suppressed. As a result, developers need to inline the fully qualified class name for every usage of a deprecated class before they can update their compile SDK version.
- Deprecation warnings can't be suppressed globally or baselined, so developers using
- Documentation on
d.android.comshows a deprecation notice. - IDEs like Android Studio show a warning at the API usage site.
- IDEs might down-rank or hide the API from auto-complete.
As a result, deprecating an API can discourage the developers who are the most concerned about code health (those using -Werror ) from adopting new SDKs. Developers who aren't concerned about warnings in their existing code are likely to ignore deprecations altogether.
An SDK that introduces a large number of deprecations makes both of these cases worse.
For this reason, we recommend deprecating APIs only in cases where:
- We plan to
@removethe API in a future release. - API use leads to incorrect or undefined behavior that we can't fix without breaking compatibility.
When you deprecate an API and replace it with a new API, we strongly recommend adding a corresponding compatibility API to a Jetpack library like androidx.core to simplify supporting both old and new devices.
We don't recommend deprecating APIs that work as intended in current and future releases:
/**
* ...
* @deprecated Use {@link #doThing(int, Bundle)} instead.
*/
@Deprecated
public void doThing(int action) {
...
}
public void doThing(int action, @Nullable Bundle extras) {
...
}
Deprecation is appropriate in cases where APIs can no longer maintain their documented behaviors:
/**
* ...
* @deprecated No longer displayed in the status bar as of API 21.
*/
@Deprecated
public RemoteViews tickerView;
Changes to deprecated APIs
You must maintain the behavior of deprecated APIs. This means test implementations must remain the same, and tests must continue to pass after you have deprecated the API. If the API doesn't have tests, you should add tests.
Don't expand deprecated API surfaces in future releases. You can add lint correctness annotations (for example, @Nullable ) to an existing deprecated API, but shouldn't add new APIs.
Don't add new APIs as deprecated. If any APIs were added and subsequently deprecated within a prerelease cycle (thus would initially enter the public API surface as deprecated), you must remove them before finalizing the API.
Soft removal
Soft removal is a source-breaking change, and you should avoid it in public APIs unless the API Council explicitly approves it. For system APIs, you must deprecate the API for the duration of a major release before a soft removal. Remove all docs references to the APIs and use the @removed <summary> docs annotation when soft-removing APIs. Your summary must include the reason for removal and can include a migration strategy, as we explained in Deprecation .
The behavior of soft-removed APIs can be maintained as is, but more importantly must be preserved such that existing callers won't crash when calling the API. In some cases, that might mean preserving behavior.
Test coverage must be maintained, but the content of the tests might need to change to accommodate for behavioral changes. Tests must still validate that existing callers don't crash at run time. You can maintain the behavior of soft-removed APIs as is, but more importantly, you must preserve it such that existing callers won't crash when calling the API. In some cases, that might mean preserving behavior.
You must maintain test coverage, but the content of the tests might need to change to accommodate behavioral changes. Tests must still validate that existing callers don't crash at run time.
At a technical level, we remove the API from the SDK stub JAR and compile-time classpath using the @remove Javadoc annotation, but it still exists on the run-time classpath -- similar to @hide APIs:
/**
* Ringer volume. This is ...
*
* @removed Not functional since API 2.
*/
public static final String VOLUME_RING = ...
From an app developer perspective, the API no longer appears in auto-complete and source code that references the API won't compile when the compileSdk is equal to or later than the SDK at which the API was removed; however, source code continues to compile successfully against earlier SDKs and binaries that reference the API continue to work.
Certain categories of API must not be soft removed. You must not soft remove certain categories of API.
روشهای انتزاعی
You must not soft remove abstract methods on classes that developers might extend. Doing so makes it impossible for developers to successfully extend the class across all SDK levels.
In rare cases where it was never and won't be possible for developers to extend a class, you can still soft remove abstract methods.
Hard removal
Hard removal is a binary-breaking change and should never occur in public APIs.
Discouraged annotation
We use the @Discouraged annotation to indicate that we don't recommend an API in most (>95%) cases. Discouraged APIs differ from deprecated APIs in that there exists a narrow critical use case that prevents deprecation. When you mark an API as discouraged, you must provide an explanation and an alternative solution:
@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);
}
You shouldn't add new APIs as discouraged.
Changes to the behavior of existing APIs
In some cases, you might want to change the implementation behavior of an existing API. For example, in Android 7.0 we improved DropBoxManager to clearly communicate when developers tried posting events that were too large to send across Binder .
However, to avoid causing problems for existing apps, we strongly recommend preserving a safe behavior for older apps. We've historically guarded these behavior changes based on the ApplicationInfo.targetSdkVersion of the app, but we've recently migrated to require using the App Compatibility Framework. Here's an example of how to implement a behavior change using this new framework:
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
}
}
}
Using this App Compatibility Framework design enables developers to temporarily disable specific behavior changes during preview and beta releases as part of debugging their apps, instead of forcing them to adjust to dozens of behavior changes simultaneously.
Forward compatibility
Forward compatibility is a design characteristic that allows a system to accept input intended for a later version of itself. In the case of API design, you must pay special attention to the initial design as well as future changes because developers expect to write code once, test it once, and have it run everywhere without issue.
The following cause the most common forward-compatibility issues in Android:
- Adding new constants to a set (such as
@IntDeforenum) previously assumed to be complete (for example, whereswitchhas adefaultthat throws an exception). - Adding support for a feature that isn't captured directly in the API surface (for example, support for assigning
ColorStateList-type resources in XML where previously only<color>resources were supported). - Loosening restrictions on run-time checks, for example removing a
requireNotNull()check that was present on lower versions.
In all of these cases, developers find out that something is wrong only at run time. Worse, they might find out as a result of crash reports from older devices in the field.
Additionally, these cases are all technically valid API changes. They don't break binary or source compatibility and API lint won't catch any of these issues.
As a result, API designers must pay careful attention when modifying existing classes. Ask the question, "Is this change going to cause code that's written and tested only against the latest version of the platform to fail on lower versions?"
XML schemas
If an XML schema serves as a stable interface between components, that schema must be explicitly specified and must evolve in a backward-compatible manner, similar to other Android APIs. For example, the structure of XML elements and attributes must be preserved similar to how methods and variables are maintained on other Android API surfaces.
XML deprecation
If you'd like to deprecate an XML element or attribute, you can add the xs:annotation marker, but you must continue to support any existing XML files by following the typical @SystemApi evolution lifecycle.
<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>
Element types must be preserved
Schemas support the sequence element, choice element and all elements as child elements of complexType element. However, these child elements differ in the number and order of their child elements, so modifying an existing type would be an incompatible change.
If you want to modify an existing type, the best-practice is to deprecate the old type and introduce a new type to replace it.
<!-- 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>
Mainline-specific patterns
Mainline is a project to allow updating subsystems ("mainline modules") of the Android OS individually, rather than updating the whole system image.
Mainline modules have to be "unbundled" from the core platform, which means all the interactions between each module and the rest of the world have to be done using formal (public or system) APIs.
There are certain design patterns mainline modules should follow. This section describes them.
The <Module>FrameworkInitializer pattern
If a mainline module needs to exposes @SystemService classes (for example, JobScheduler ) then use the following pattern:
Expose a
<YourModule>FrameworkInitializerclass from your module. This class needs to be in$BOOTCLASSPATH. Example: StatsFrameworkInitializerMark it with
@SystemApi(client = MODULE_LIBRARIES).Add a
public static void registerServiceWrappers()method to it.Use
SystemServiceRegistry.registerContextAwareService()to register a service manager class when it needs a reference to aContext.Use
SystemServiceRegistry.registerStaticService()to register a service manager class when it doesn't need a reference to aContext.Call the
registerServiceWrappers()method fromSystemServiceRegistry's static initializer.
The <Module>ServiceManager pattern
Normally, in order to register system service binder objects or get references to them, one would use ServiceManager , but mainline modules can't use it because it's hidden. This class is hidden because mainline modules aren't supposed to register or refer to system service binder objects exposed by the static platform or by other modules.
Mainline modules can use the following pattern instead to be able to register and get references to binder services that are implemented inside the module.
Create a
<YourModule>ServiceManagerclass, following the design of TelephonyServiceManagerExpose the class as
@SystemApi. If you only need to access it from$BOOTCLASSPATHclasses or system server classes, you can use@SystemApi(client = MODULE_LIBRARIES); otherwise@SystemApi(client = PRIVILEGED_APPS)would work.This class would consists of:
- A hidden constructor, so only the static platform code can instantiate it.
- Public getter methods that return a
ServiceRegistererinstance for a specific name. If you have one binder object, then you need one getter method. If you have two, then you need two getters. - In
ActivityThread.initializeMainlineModules(), instantiate this class, and pass it to a static method exposed by your module. Normally, you add a static@SystemApi(client = MODULE_LIBRARIES)API in yourFrameworkInitializerclass that takes it.
This pattern would prevent other mainline modules from accessing these APIs because there's no way for other modules to get an instance of <YourModule>ServiceManager , even though the get() and register() APIs are visible to them.
Here is how telephony gets a reference to the telephony service: code search link .
If your implements a service binder object in native code, you use the AServiceManager native APIs . These APIs correspond to the ServiceManager Java APIs but the native ones are directly exposed to mainline modules. Don't use them to register or refer to binder objects that aren't owned by your module. If you expose a binder object from native, your <YourModule>ServiceManager.ServiceRegisterer doesn't need a register() method.
Permission definitions in Mainline modules
Mainline modules containing APKs may define (custom) permissions in their APK AndroidManifest.xml in the same way as a regular APK.
If the defined permission is only used internally within a module, its permission name should be prefixed with the APK package name, for example:
<permission
android:name="com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER"
android:protectionLevel="signature" />
If the defined permission is to be provided as part of an updatable platform API to other apps, its permission name should be prefixed with "android.permission." (like any static platform permission) plus the module package name, to signal it's a platform API from a module while avoiding any naming conflicts, for example:
<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" />
Then the module can expose this permission name as an API constant in its API surface, for example HealthPermissions.READ_ACTIVE_CALORIES_BURNED .