หลักเกณฑ์ Android API

หน้านี้มีไว้เพื่อเป็นคำแนะนำสำหรับนักพัฒนาซอฟต์แวร์ในการทำความเข้าใจหลักการทั่วไป ที่สภา API บังคับใช้ในการตรวจสอบ API

นอกเหนือจากการปฏิบัติตามหลักเกณฑ์เหล่านี้เมื่อเขียน API แล้ว นักพัฒนาซอฟต์แวร์ควรเรียกใช้เครื่องมือ API Lint ซึ่งจะเข้ารหัสกฎเหล่านี้หลายข้อในการตรวจสอบที่เรียกใช้กับ API

คิดว่านี่คือคำแนะนำเกี่ยวกับกฎที่เครื่องมือ Lint นั้นๆ ปฏิบัติตาม รวมถึง คำแนะนำทั่วไปเกี่ยวกับกฎที่เข้ารหัสลงในเครื่องมือดังกล่าวไม่ได้ด้วยความแม่นยำสูง

เครื่องมือ Lint API

API Lint ผสานรวมอยู่ในเครื่องมือวิเคราะห์แบบคงที่ของ Metalava และจะทำงานโดยอัตโนมัติ ระหว่างการตรวจสอบใน CI คุณเรียกใช้ด้วยตนเองจากการชำระเงินของแพลตฟอร์มในเครื่องโดยใช้ m checkapi หรือการชำระเงิน AndroidX ในเครื่องโดยใช้ ./gradlew :path:to:project:checkApi ได้

กฎ API

แพลตฟอร์ม Android และไลบรารี Jetpack หลายรายการมีอยู่ก่อนที่จะมีการสร้างชุด หลักเกณฑ์นี้ และนโยบายที่ระบุไว้ในหน้าเว็บนี้ในภายหลัง มีการพัฒนาอย่างต่อเนื่องเพื่อให้ตรงกับความต้องการของระบบนิเวศ Android

ดังนั้น API ที่มีอยู่บางรายการอาจไม่เป็นไปตามหลักเกณฑ์ ในกรณีอื่นๆ การที่ API ใหม่สอดคล้องกับ API ที่มีอยู่แทนที่จะยึดตามหลักเกณฑ์อย่างเคร่งครัดอาจช่วยให้นักพัฒนาแอปได้รับประสบการณ์การใช้งานที่ดีขึ้น

ใช้ดุลยพินิจและติดต่อสภา API หากมีคำถามที่ตอบยากเกี่ยวกับ API ที่ต้องแก้ไขหรือหลักเกณฑ์ที่ต้องอัปเดต

ข้อมูลเบื้องต้นเกี่ยวกับ API

หมวดหมู่นี้เกี่ยวข้องกับแง่มุมหลักของ Android API

ต้องใช้งาน API ทั้งหมด

ไม่ว่ากลุ่มเป้าหมายของ API จะเป็นใคร (เช่น สาธารณะหรือ @SystemApi) คุณต้องใช้ API ทั้งหมดเมื่อผสานรวมหรือแสดงเป็น API อย่าผสานรวม API Stubs กับการติดตั้งใช้งานที่จะมีในภายหลัง

API ที่ไม่มีการใช้งานมีปัญหาหลายอย่างดังนี้

  • ไม่มีการรับประกันว่าพื้นผิวที่เหมาะสมหรือสมบูรณ์จะได้รับการเปิดเผย จนกว่าไคลเอ็นต์จะทดสอบหรือใช้ API จะไม่มีวิธีตรวจสอบว่าไคลเอ็นต์มี API ที่เหมาะสมเพื่อให้ใช้ฟีเจอร์ได้
  • คุณจะทดสอบ API ที่ไม่มีการติดตั้งใช้งานในรุ่นตัวอย่างสำหรับนักพัฒนาแอปไม่ได้
  • API ที่ไม่มีการติดตั้งใช้งานจะทดสอบใน CTS ไม่ได้

ต้องทดสอบ API ทั้งหมด

ซึ่งสอดคล้องกับข้อกำหนด CTS ของแพลตฟอร์ม นโยบาย AndroidX และแนวคิดทั่วไปที่ว่าต้องมีการใช้งาน API

การทดสอบ API Surface เป็นการรับประกันพื้นฐานว่า API Surface ใช้งานได้ และเราได้จัดการกับ Use Case ที่คาดไว้แล้ว การทดสอบการมีอยู่ไม่เพียงพอ คุณต้องทดสอบลักษณะการทำงานของ API เอง

การเปลี่ยนแปลงที่เพิ่ม API ใหม่ควรมีการทดสอบที่เกี่ยวข้องใน CL เดียวกัน หรือหัวข้อ Gerrit

นอกจากนี้ API ควรทดสอบได้ด้วย คุณควรตอบคำถามที่ว่า "นักพัฒนาแอปจะทดสอบโค้ดที่ใช้ API ของคุณอย่างไร" ได้

ต้องจัดทำเอกสาร API ทั้งหมด

เอกสารประกอบเป็นส่วนสำคัญของความสามารถในการใช้งาน API แม้ว่าไวยากรณ์ของ API Surface อาจดูชัดเจน แต่ไคลเอ็นต์ใหม่จะไม่เข้าใจความหมาย ลักษณะการทำงาน หรือ บริบทเบื้องหลัง API

API ที่สร้างขึ้นทั้งหมดต้องเป็นไปตามหลักเกณฑ์

API ที่สร้างโดยเครื่องมือต้องเป็นไปตามหลักเกณฑ์ API เดียวกันกับโค้ดที่เขียนด้วยมือ

เครื่องมือที่ไม่แนะนำให้ใช้ในการสร้าง API

  • AutoValue: ละเมิดหลักเกณฑ์ในหลายๆ ด้าน เช่น ไม่มีวิธีใช้คลาสค่าสุดท้ายหรือบิลเดอร์สุดท้ายกับวิธีที่ AutoValue ทำงาน

รูปแบบโค้ด

หมวดหมู่นี้เกี่ยวข้องกับรูปแบบโค้ดทั่วไปที่นักพัฒนาซอฟต์แวร์ควรใช้ โดยเฉพาะอย่างยิ่งเมื่อเขียน API สาธารณะ

ปฏิบัติตามหลักการเขียนโค้ดมาตรฐาน ยกเว้นในกรณีที่ระบุไว้

เอกสารเกี่ยวกับหลักเกณฑ์การเขียนโค้ด Android สำหรับผู้ร่วมให้ข้อมูลภายนอกมีดังนี้

https://source.android.com/source/code-style.html

โดยรวมแล้ว เรามักจะปฏิบัติตามรูปแบบการเขียนโค้ด Java และ Kotlin มาตรฐาน

ไม่ควรใช้อักษรตัวพิมพ์ใหญ่ในชื่อเมธอด

เช่น ชื่อเมธอดควรเป็น runCtsTests ไม่ใช่ runCTSTests

ชื่อไม่ควรลงท้ายด้วย Impl

ซึ่งจะแสดงรายละเอียดการใช้งาน ดังนั้นโปรดหลีกเลี่ยง

ชั้นเรียน

ส่วนนี้อธิบายกฎเกี่ยวกับคลาส อินเทอร์เฟซ และการสืบทอด

รับช่วงคลาสสาธารณะใหม่จากคลาสฐานที่เหมาะสม

การรับค่าจะแสดงองค์ประกอบ API ในคลาสย่อยซึ่งอาจไม่เหมาะสม เช่น คลาสย่อยสาธารณะใหม่ของ FrameLayout จะมีลักษณะเป็น FrameLayout รวมถึงลักษณะการทำงานและองค์ประกอบ API ใหม่ หาก API ที่รับช่วงมาไม่เหมาะสม สำหรับ Use Case ของคุณ ให้รับช่วงจากคลาสที่อยู่สูงขึ้นไปในโครงสร้าง เช่น ViewGroup หรือ View

หากคุณต้องการแทนที่เมธอดจากคลาสฐานเพื่อส่ง UnsupportedOperationException ให้ลองพิจารณาคลาสฐานที่คุณใช้อีกครั้ง

ใช้คลาสคอลเล็กชันพื้นฐาน

ไม่ว่าจะใช้คอลเล็กชันเป็นอาร์กิวเมนต์หรือส่งคืนเป็นค่า ให้เลือกใช้คลาสฐานแทนการใช้งานที่เฉพาะเจาะจงเสมอ (เช่น ส่งคืน List<Foo> แทน ArrayList<Foo>)

ใช้คลาสฐานที่แสดงข้อจำกัดที่เหมาะสมสำหรับ API เช่น ใช้ List สำหรับ API ที่ต้องจัดลำดับคอลเล็กชัน และใช้ Set สำหรับ API ที่คอลเล็กชันต้องประกอบด้วยองค์ประกอบที่ไม่ซ้ำกัน

ใน Kotlin ให้ใช้คอลเล็กชันที่ไม่เปลี่ยนแปลง ดูรายละเอียดเพิ่มเติมได้ที่การเปลี่ยนแปลงคอลเล็กชัน

คลาสแอบสแทรกต์เทียบกับอินเทอร์เฟซ

Java 8 เพิ่มการรองรับเมธอดอินเทอร์เฟซเริ่มต้น ซึ่งช่วยให้นักออกแบบ API เพิ่มเมธอดลงในอินเทอร์เฟซได้ในขณะที่ยังคงความเข้ากันได้แบบไบนารี โค้ดแพลตฟอร์ม และไลบรารี Jetpack ทั้งหมดควรกำหนดเป้าหมายเป็น Java 8 ขึ้นไป

ในกรณีที่การติดตั้งใช้งานเริ่มต้นเป็นแบบไม่มีสถานะ ผู้ออกแบบ API ควร เลือกใช้อินเทอร์เฟซมากกว่าคลาสแบบนามธรรม กล่าวคือ เมธอดอินเทอร์เฟซเริ่มต้น สามารถติดตั้งใช้งานเป็นการเรียกเมธอดอินเทอร์เฟซอื่นๆ ได้

ในกรณีที่การติดตั้งใช้งานเริ่มต้นต้องมีตัวสร้างหรือสถานะภายใน จะต้องใช้คลาสแบบนามธรรม

ในทั้ง 2 กรณี นักออกแบบ API สามารถเลือกที่จะปล่อยให้เมธอดเดียวเป็นแบบแอบสแทรกต์เพื่อ ลดความซับซ้อนในการใช้งานเป็น Lambda ได้

public interface AnimationEndCallback {
  // Always called, must be implemented.
  public void onFinished(Animation anim);
  // Optional callbacks.
  public default void onStopped(Animation anim) { }
  public default void onCanceled(Animation anim) { }
}

ชื่อคลาสควรแสดงถึงสิ่งที่ขยาย

ตัวอย่างเช่น คลาสที่ขยาย Service ควรตั้งชื่อเป็น FooService เพื่อให้ ชัดเจน

public class IntentHelper extends Service {}
public class IntentService extends Service {}

คำเสริมท้ายทั่วไป

หลีกเลี่ยงการใช้คำต่อท้ายชื่อคลาสทั่วไป เช่น Helper และ Util สำหรับคอลเล็กชัน ของเมธอดอรรถประโยชน์ แต่ให้ใส่เมธอดในคลาสที่เชื่อมโยงโดยตรง หรือในฟังก์ชันส่วนขยาย Kotlin

ในกรณีที่เมธอดเชื่อมต่อหลายคลาส ให้ตั้งชื่อคลาสที่มีความหมายซึ่งอธิบายสิ่งที่เมธอดทำ

ในบางกรณีที่จำกัดมาก การใช้คำต่อท้าย Helper อาจเหมาะสม

  • ใช้สำหรับการเรียบเรียงลักษณะการทำงานเริ่มต้น
  • อาจเกี่ยวข้องกับการมอบสิทธิ์ลักษณะการทำงานที่มีอยู่ให้กับคลาสใหม่
  • อาจต้องใช้สถานะที่คงอยู่
  • โดยปกติจะใช้เวลา View

ตัวอย่างเช่น หากการย้อนพอร์ตเคล็ดลับเครื่องมือต้องเก็บสถานะที่เชื่อมโยง กับ View และเรียกใช้หลายเมธอดใน View เพื่อติดตั้งการย้อนพอร์ต TooltipHelper จะเป็นชื่อคลาสที่ยอมรับได้

อย่าแสดงโค้ดที่สร้างโดย IDL เป็น API สาธารณะโดยตรง

เก็บโค้ดที่สร้างโดย IDL ไว้เป็นรายละเอียดการใช้งาน ซึ่งรวมถึง Protobuf, ซ็อกเก็ต, FlatBuffers หรือพื้นผิว API อื่นๆ ที่ไม่ใช่ Java และไม่ใช่ NDK อย่างไรก็ตาม IDL ส่วนใหญ่ใน Android จะอยู่ใน AIDL ดังนั้นหน้านี้จึงเน้นที่ AIDL

คลาส AIDL ที่สร้างขึ้นไม่เป็นไปตามข้อกำหนดของคู่มือรูปแบบ API (เช่น ใช้การโอเวอร์โหลดไม่ได้) และเครื่องมือ AIDL ไม่ได้ออกแบบมาโดยเฉพาะเพื่อ รักษาความเข้ากันได้ของ API ภาษา คุณจึงฝังคลาสเหล่านี้ใน API สาธารณะไม่ได้

แต่ให้เพิ่มเลเยอร์ API สาธารณะไว้เหนืออินเทอร์เฟซ AIDL แม้ว่าจะเป็น Wrapper แบบตื้นๆ ในตอนแรกก็ตาม

อินเทอร์เฟซ Binder

หากBinderอินเทอร์เฟซเป็นรายละเอียดการใช้งาน ก็สามารถเปลี่ยนแปลงได้อย่างอิสระ ในอนาคต โดยเลเยอร์สาธารณะจะช่วยให้สามารถรักษาความเข้ากันได้แบบย้อนหลังที่จำเป็น ไว้ได้ เช่น คุณอาจต้องเพิ่มอาร์กิวเมนต์ใหม่ ในการเรียกภายใน หรือเพิ่มประสิทธิภาพการรับส่งข้อมูล IPC โดยใช้ การจัดกลุ่มหรือการสตรีม การใช้หน่วยความจำที่ใช้ร่วมกัน หรือสิ่งที่คล้ายกัน คุณจะทำสิ่งเหล่านี้ไม่ได้ หากอินเทอร์เฟซ AIDL เป็น API สาธารณะด้วย

เช่น อย่าเปิดเผย FooService เป็น API สาธารณะโดยตรง

// BAD: Public API generated from IFooService.aidl
public class IFooService {
   public void doFoo(String foo);
}

แต่ให้ห่อหุ้มอินเทอร์เฟซ Binder ไว้ภายในเครื่องมือจัดการหรือคลาสอื่นๆ แทน

/**
 * @hide
 */
public class IFooService {
   public void doFoo(String foo);
}

public IFooManager {
   public void doFoo(String foo) {
      mFooService.doFoo(foo);
   }
}

หากในภายหลังจำเป็นต้องมีอาร์กิวเมนต์ใหม่สำหรับการเรียกนี้ อินเทอร์เฟซภายในจะ มีการโอเวอร์โหลดที่น้อยที่สุดและสะดวกซึ่งเพิ่มลงใน 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อินเทอร์เฟซที่ไม่ได้เป็นส่วนหนึ่งของแพลตฟอร์ม Android (เช่น อินเทอร์เฟซบริการที่ส่งออกจากบริการ Google Play เพื่อให้แอปใช้งาน) ข้อกำหนดสำหรับอินเทอร์เฟซ IPC ที่เสถียร เผยแพร่แล้ว และมีการควบคุมเวอร์ชันหมายความว่าการพัฒนาอินเทอร์เฟซนั้นทำได้ยากขึ้นมาก อย่างไรก็ตาม คุณยังคงควรมีเลเยอร์ Wrapper รอบๆ เพื่อให้เป็นไปตามหลักเกณฑ์ 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 คลาส Manager จะสื่อสารกับบริการของระบบและเป็นจุดโต้ตอบเพียงจุดเดียว ไม่จำเป็นต้อง ปรับแต่ง จึงประกาศเป็น final

อย่าใช้ CompletableFuture หรือ Future

java.util.concurrent.CompletableFuture มี API จำนวนมากที่อนุญาตให้ แก้ไขค่าในอนาคตได้ตามต้องการและมีค่าเริ่มต้นที่อาจเกิดข้อผิดพลาด

ในทางกลับกัน java.util.concurrent.Future ไม่มีฟังก์ชันการฟังแบบไม่บล็อก จึงทำให้ใช้งานกับโค้ดแบบอะซิงโครนัสได้ยาก

ในโค้ดแพลตฟอร์มและAPI ของไลบรารีระดับล่างที่ทั้ง Kotlin และ Java ใช้ ให้ใช้การเรียกกลับการดำเนินการเสร็จสมบูรณ์ Executor ร่วมกัน และหาก API รองรับการยกเลิก CancellationSignal

public void asyncLoadFoo(android.os.CancellationSignal cancellationSignal,
    Executor callbackExecutor,
    android.os.OutcomeReceiver<FooResult, Throwable> callback);

หากกำหนดเป้าหมายเป็น Kotlin ให้ใช้ฟังก์ชัน suspend

suspend fun asyncLoadFoo(): Foo

ในไลบรารีการผสานรวมเฉพาะ Java คุณสามารถใช้ ListenableFuture ของ Guava ได้

public com.google.common.util.concurrent.ListenableFuture<Foo> asyncLoadFoo();

อย่าใช้ Optional

แม้ว่า Optional จะมีข้อดีใน API บางอย่าง แต่ก็ไม่สอดคล้องกับพื้นที่ API ของ Android ที่มีอยู่ @Nullable และ @NonNull ให้ความช่วยเหลือด้านเครื่องมือสำหรับความปลอดภัยของ null และ Kotlin บังคับใช้สัญญา Nullability ที่ระดับคอมไพเลอร์ ทำให้ Optional ไม่จำเป็น

สำหรับ Primitive ที่ไม่บังคับ ให้ใช้วิธีการ has และ get ที่จับคู่กัน หากไม่ได้ตั้งค่า (has แสดงผล false) เมธอด get ควรแสดง IllegalStateException

public boolean hasAzimuth() { ... }
public int getAzimuth() {
  if (!hasAzimuth()) {
    throw new IllegalStateException("azimuth is not set");
  }
  return azimuth;
}

ใช้ตัวสร้างส่วนตัวสำหรับคลาสที่สร้างอินสแตนซ์ไม่ได้

คลาสที่สร้างได้โดย Builder เท่านั้น คลาสที่มีเฉพาะค่าคงที่หรือเมธอดแบบคงที่ หรือคลาสที่สร้างอินสแตนซ์ไม่ได้ ควรมีตัวสร้างแบบส่วนตัวอย่างน้อย 1 รายการเพื่อป้องกันการสร้างอินสแตนซ์โดยใช้ตัวสร้างเริ่มต้นที่ไม่มีอาร์กิวเมนต์

public final class Log {
  // Not instantiable.
  private Log() {}
}

Singleton

เราไม่แนะนำให้ใช้ Singleton เนื่องจากมีข้อเสียที่เกี่ยวข้องกับการทดสอบดังต่อไปนี้

  1. การก่อสร้างได้รับการจัดการโดยชั้นเรียน ซึ่งป้องกันการใช้ของปลอม
  2. การทดสอบไม่สามารถเป็นแบบเฮอร์เมติกได้เนื่องจากลักษณะแบบคงที่ของ Singleton
  3. หากต้องการแก้ปัญหาเหล่านี้ นักพัฒนาแอปต้องทราบรายละเอียดภายในของ Singleton หรือสร้าง Wrapper รอบ Singleton

ใช้รูปแบบอินสแตนซ์เดียว ซึ่งอาศัย คลาสฐานแบบนามธรรมเพื่อแก้ไขปัญหาเหล่านี้

อินสแตนซ์เดียว

คลาสอินสแตนซ์เดียวใช้คลาสฐานแบบนามธรรมที่มีตัวสร้าง 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
    }
  }
}

อินสแตนซ์เดียวแตกต่างจากซิงเกิลตันตรงที่นักพัฒนาแอป สามารถสร้างเวอร์ชันปลอมของ SingleInstance และใช้เฟรมเวิร์กการแทรก Dependency ของตนเองเพื่อจัดการการติดตั้งใช้งานได้โดยไม่ต้องสร้าง Wrapper หรือไลบรารีสามารถระบุเวอร์ชันปลอมของตนเองในอาร์ติแฟกต์ -testing ได้

คลาสที่เผยแพร่ทรัพยากรควรใช้ AutoCloseable

คลาสที่ปล่อยทรัพยากรผ่าน close, release, destroy หรือเมธอดที่คล้ายกัน ควรใช้ java.lang.AutoCloseable เพื่อให้นักพัฒนาแอป ล้างทรัพยากรเหล่านี้โดยอัตโนมัติเมื่อใช้บล็อก try-with-resources

หลีกเลี่ยงการเปิดตัวคลาสย่อย View ใหม่ใน android.*

อย่าสร้างคลาสใหม่ที่รับค่าโดยตรงหรือโดยอ้อมจาก android.view.View ใน API สาธารณะของแพลตฟอร์ม (นั่นคือใน android.*)

ตอนนี้ชุดเครื่องมือ UI ของ Android เป็น Compose-first แล้ว ฟีเจอร์ UI ใหม่ที่แพลตฟอร์มแสดงควรแสดงเป็น API ระดับล่างที่ใช้เพื่อติดตั้งใช้งาน Jetpack Compose และคอมโพเนนต์ UI ที่อิงตาม View (ไม่บังคับ) สำหรับนักพัฒนาแอปในไลบรารี Jetpack การเสนอคอมโพเนนต์เหล่านี้ในไลบรารี ช่วยให้มีโอกาสในการติดตั้งใช้งานแบบย้อนกลับเมื่อฟีเจอร์ของแพลตฟอร์ม ไม่พร้อมใช้งาน

ช่อง

กฎเหล่านี้เกี่ยวข้องกับฟิลด์สาธารณะในคลาส

อย่าแสดงฟิลด์ดิบ

คลาส Java ไม่ควรแสดงฟิลด์โดยตรง ฟิลด์ควรเป็นแบบส่วนตัวและเข้าถึงได้โดยใช้ Getter และ Setter สาธารณะเท่านั้น ไม่ว่าฟิลด์เหล่านี้จะเป็นแบบสุดท้ายหรือไม่ก็ตาม

ข้อยกเว้นที่พบได้ยาก ได้แก่ โครงสร้างข้อมูลพื้นฐานที่ไม่จำเป็นต้องปรับปรุง ลักษณะการทำงานของการระบุหรือดึงข้อมูลฟิลด์ ในกรณีดังกล่าว ควรตั้งชื่อฟิลด์โดยใช้รูปแบบการตั้งชื่อตัวแปรมาตรฐาน เช่น Point.x และ Point.y

คลาส Kotlin สามารถแสดงพร็อพเพอร์ตี้ได้

ควรทำเครื่องหมายฟิลด์ที่เปิดเผยเป็น final

เราไม่แนะนำให้ใช้ฟิลด์ดิบ (@see อย่าแสดงฟิลด์ดิบ) แต่ในกรณีที่พบไม่บ่อยซึ่งมีการเปิดเผยฟิลด์ เป็นฟิลด์สาธารณะ ให้ทำเครื่องหมายฟิลด์นั้นเป็น final

ไม่ควรเปิดเผยฟิลด์ภายใน

อย่าอ้างอิงชื่อฟิลด์ภายในใน API สาธารณะ

public int mFlags;

ใช้สาธารณะแทนที่ได้รับการป้องกัน

@see ใช้สาธารณะแทนที่ได้รับการปกป้อง

ค่าคงที่

กฎเหล่านี้เป็นกฎเกี่ยวกับค่าคงที่สาธารณะ

ค่าคงที่ของ Flag ไม่ควรทับซ้อนกับค่า int หรือ long

แฟล็กหมายถึงบิตที่รวมกันเป็นค่าสหภาพได้ หากไม่ใช่กรณีนี้ อย่าเรียกตัวแปรหรือค่าคงที่ว่า 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สำหรับแฟล็กบิตมาสก์

ค่าคงที่แบบคงที่สุดท้ายควรใช้รูปแบบการตั้งชื่อแบบตัวพิมพ์ใหญ่ทั้งหมดโดยมีขีดล่างคั่น

คำทั้งหมดในค่าคงที่ควรเป็นตัวพิมพ์ใหญ่ และคำหลายคำควรคั่นด้วย _ เช่น

public static final int fooThing = 5
public static final int FOO_THING = 5

ใช้คำนำหน้ามาตรฐานสำหรับค่าคงที่

ค่าคงที่หลายรายการที่ใช้ใน Android มีไว้สำหรับสิ่งต่างๆ ที่เป็นมาตรฐาน เช่น แฟล็ก คีย์ และการดำเนินการ ค่าคงที่เหล่านี้ควรมีคำนำหน้ามาตรฐานเพื่อให้ระบุได้ง่ายขึ้นว่าเป็นค่าคงที่

เช่น ส่วนเสริมของ Intent ควรขึ้นต้นด้วย EXTRA_ การดำเนินการตามความตั้งใจควรขึ้นต้นด้วย ACTION_ ค่าคงที่ที่ใช้กับ Context.bindService() ควรขึ้นต้นด้วย BIND_

ชื่อและขอบเขตของค่าคงที่หลัก

ค่าของค่าคงที่สตริงควรสอดคล้องกับชื่อค่าคงที่เอง และ โดยทั่วไปควรมีขอบเขตเป็นแพ็กเกจหรือโดเมน เช่น

public static final String FOO_THING = "foo"

ไม่ได้ตั้งชื่ออย่างสอดคล้องกันหรือไม่ได้กำหนดขอบเขตอย่างเหมาะสม ให้ลองทำสิ่งต่อไปนี้แทน

public static final String FOO_THING = "android.fooservice.FOO_THING"

คำนำหน้าของ android ในค่าคงที่สตริงที่กำหนดขอบเขตจะสงวนไว้สำหรับโปรเจ็กต์โอเพนซอร์สของ Android

การดำเนินการและส่วนเสริมของ 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"
}

ใช้สาธารณะแทนที่ได้รับการป้องกัน

@see ใช้สาธารณะแทนที่ได้รับการปกป้อง

ใช้คำนำหน้าที่สอดคล้องกัน

ค่าคงที่ที่เกี่ยวข้องทั้งหมดควรเริ่มต้นด้วยคำนำหน้าเดียวกัน ตัวอย่างเช่น สำหรับชุด ค่าคงที่ที่จะใช้กับค่าสถานะ

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 ซึ่งคล้ายกับฟิลด์สาธารณะใน Java

ในบางกรณี ตัวระบุหรือแอตทริบิวต์สาธารณะจะมีคำนำหน้าที่พบบ่อย ซึ่งคั่นด้วยขีดล่าง

  • ค่าการกำหนดค่าแพลตฟอร์ม เช่น @string/config_recentsComponentName ใน config.xml
  • แอตทริบิวต์มุมมองเฉพาะเลย์เอาต์ เช่น @attr/layout_marginStart ใน attrs.xml

ธีมและสไตล์สาธารณะต้องเป็นไปตามรูปแบบการตั้งชื่อ PascalCase แบบลำดับชั้น เช่น @style/Theme.Material.Light.DarkActionBar หรือ @style/Widget.Material.SearchView.ActionBar ซึ่งคล้ายกับคลาสที่ซ้อนกันใน Java

ไม่ควรเปิดเผยทรัพยากรเลย์เอาต์และ Drawable เป็น API สาธารณะ อย่างไรก็ตาม หากต้องเปิดเผยเลย์เอาต์และ Drawable สาธารณะ ต้องตั้งชื่อโดยใช้รูปแบบการตั้งชื่อแบบ 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 ระดับ 22 และ มีการคาดการณ์ที่สมเหตุสมผล (ในระดับหนึ่ง) ว่ามีสถานะที่เป็นไปได้เพียง 2 สถานะ เท่านั้น แต่หากแอปได้รับ 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 ที่ ดูเหมือนค่าคงที่แบบครอบคลุม นักพัฒนาแอปจะถือว่าค่าคงที่ที่เผยแพร่เมื่อ เขียนแอปของตนนั้นครอบคลุมทั้งหมด หากคุณไม่ต้องการตั้งค่าความคาดหวังนี้ โปรดพิจารณาอีกครั้งว่าค่าคงที่แบบครอบคลุมทั้งหมดเหมาะกับ API ของคุณหรือไม่

นอกจากนี้ ไลบรารียังระบุ targetSdkVersion ของตัวเองแยกจาก แอปไม่ได้ และการจัดการการเปลี่ยนแปลงลักษณะการทำงานของ targetSdkVersion จากโค้ดของไลบรารีก็ ซับซ้อนและมีแนวโน้มที่จะเกิดข้อผิดพลาด

ค่าคงที่จำนวนเต็มหรือสตริง

ใช้ค่าคงที่จำนวนเต็มและ @IntDef หากเนมสเปซสำหรับค่าไม่ ขยายได้ภายนอกแพ็กเกจ ใช้ค่าคงที่สตริงหากเนมสเปซเป็น shared หรือขยายได้ด้วยโค้ดภายนอกแพ็กเกจ

คลาสข้อมูล

คลาสข้อมูลแสดงชุดพร็อพเพอร์ตี้ที่ไม่เปลี่ยนแปลง และมีชุดฟังก์ชันยูทิลิตีขนาดเล็กและ กำหนดไว้อย่างดีสำหรับการโต้ตอบกับข้อมูลนั้น

อย่าใช้ data class ใน Kotlin API สาธารณะ เนื่องจากคอมไพเลอร์ Kotlin ไม่รับประกัน API ภาษาหรือความเข้ากันได้แบบไบนารีสำหรับโค้ดที่สร้างขึ้น ให้ใช้ฟังก์ชันที่จำเป็นด้วยตนเองแทน

การสร้างอินสแตนซ์

ใน Java คลาสข้อมูลควรมีตัวสร้างเมื่อมีพร็อพเพอร์ตี้เพียงไม่กี่รายการ หรือใช้รูปแบบ Builder เมื่อมีพร็อพเพอร์ตี้จำนวนมาก

ใน Kotlin คลาสข้อมูลควรมีตัวสร้างที่มีอาร์กิวเมนต์เริ่มต้น ไม่ว่าจะมีพร็อพเพอร์ตี้กี่รายการก็ตาม คลาสข้อมูลที่กำหนดใน Kotlin อาจได้รับประโยชน์จากการระบุ Builder เมื่อกำหนดเป้าหมายไคลเอ็นต์ Java ด้วย

การแก้ไขและการคัดลอก

ในกรณีที่ต้องแก้ไขข้อมูล ให้ระบุคลาส Builder ที่มีตัวสร้างสำเนา (Java) หรือฟังก์ชันสมาชิก copy() (Kotlin) ที่แสดงผลออบเจ็กต์ใหม่

เมื่อระบุฟังก์ชัน copy() ใน Kotlin อาร์กิวเมนต์ต้องตรงกับ ตัวสร้างของคลาส และต้องป้อนค่าเริ่มต้นโดยใช้ค่าปัจจุบันของออบเจ็กต์

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() กับรูปแบบที่แนะนำ ซึ่งตรงกับการใช้งานคลาสข้อมูลของ Kotlin เช่น User(var1=Alex, var2=42)

วิธีการ

ซึ่งเป็นกฎเกี่ยวกับรายละเอียดต่างๆ ในเมธอด พารามิเตอร์ ชื่อเมธอด ประเภทการคืนค่า และตัวระบุการเข้าถึง

เวลา

กฎเหล่านี้ครอบคลุมวิธีแสดงแนวคิดเกี่ยวกับเวลา เช่น วันที่และระยะเวลา ใน API

ใช้ประเภท java.time.* หากเป็นไปได้

java.time.Duration, java.time.Instant และ java.time.* ประเภทอื่นๆ อีกมากมาย พร้อมใช้งานในแพลตฟอร์มทุกเวอร์ชันผ่าน การแปลงและ ควรใช้เมื่อแสดงเวลาในพารามิเตอร์ API หรือค่าที่ส่งคืน

ควรแสดงเฉพาะตัวแปรของ API ที่ยอมรับหรือแสดง java.time.Duration หรือ java.time.Instant และละเว้นตัวแปรดั้งเดิมที่มี Use Case เดียวกัน เว้นแต่โดเมน API จะเป็นโดเมนที่การจัดสรรออบเจ็กต์ในรูปแบบการใช้งานที่ต้องการจะส่งผลกระทบต่อประสิทธิภาพในระดับที่ห้ามใช้

วิธีการแสดงระยะเวลาควรมีชื่อว่า "ระยะเวลา"

หากค่าเวลาแสดงระยะเวลาที่เกี่ยวข้อง ให้ตั้งชื่อพารามิเตอร์เป็น "duration" ไม่ใช่ "time"

ValueAnimator.setTime(java.time.Duration);
ValueAnimator.setDuration(java.time.Duration);

ข้อยกเว้น:

"หมดเวลา" เหมาะสมเมื่อระยะเวลาใช้กับค่าหมดเวลาโดยเฉพาะ

"time" ที่มีประเภทเป็น java.time.Instant เหมาะสมเมื่ออ้างอิงถึง จุดเวลาที่เฉพาะเจาะจง ไม่ใช่ระยะเวลา

เมธอดที่แสดงระยะเวลาหรือเวลาเป็นค่าดั้งเดิมควรตั้งชื่อตามหน่วยเวลาและใช้ long

เมธอดที่ยอมรับหรือแสดงระยะเวลาเป็นค่าดั้งเดิมควรต่อท้ายชื่อเมธอดด้วยหน่วยเวลาที่เกี่ยวข้อง (เช่น Millis, Nanos, Seconds) เพื่อสงวนชื่อที่ไม่มีการตกแต่งไว้ใช้กับ java.time.Duration ดูเวลา

นอกจากนี้ คุณควรใส่คำอธิบายประกอบเมธอดอย่างเหมาะสมพร้อมกับหน่วยและฐานเวลา

  • @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();

วางพารามิเตอร์ที่ไม่บังคับไว้ท้ายการโอเวอร์โหลด

หากคุณมีโอเวอร์โหลดของเมธอดที่มีพารามิเตอร์ที่ไม่บังคับ ให้เก็บพารามิเตอร์เหล่านั้นไว้ที่ท้ายสุดและรักษลําดับให้สอดคล้องกับพารามิเตอร์อื่นๆ

public int doFoo(boolean flag);

public int doFoo(int id, boolean flag);
public int doFoo(boolean flag);

public int doFoo(boolean flag, int id);

เมื่อเพิ่มการโอเวอร์โหลดสำหรับอาร์กิวเมนต์ที่ไม่บังคับ วิธีการที่ง่ายกว่า ควรทํางานในลักษณะเดียวกับที่อาร์กิวเมนต์เริ่มต้น ได้รับการระบุในวิธีการที่ซับซ้อนกว่า

ข้อสรุป: อย่าโอเวอร์โหลดเมธอดนอกเหนือจากการเพิ่มอาร์กิวเมนต์ที่ไม่บังคับหรือเพื่อ ยอมรับอาร์กิวเมนต์ประเภทต่างๆ หากเมธอดเป็นแบบ Polymorphic หาก เมธอดที่โอเวอร์โหลดทําสิ่งที่แตกต่างออกไปโดยสิ้นเชิง ให้ตั้งชื่อใหม่

ต้องใส่คำอธิบายประกอบ @JvmOverloads ในเมธอดที่มีพารามิเตอร์เริ่มต้น (Kotlin เท่านั้น)

ต้องใส่คำอธิบายประกอบให้กับเมธอดและตัวสร้างที่มีพารามิเตอร์เริ่มต้นด้วย @JvmOverloads เพื่อรักษาความเข้ากันได้ของไบนารี

ดูรายละเอียดเพิ่มเติมได้ที่ การโอเวอร์โหลดฟังก์ชันสำหรับค่าเริ่มต้น ในคำแนะนำเกี่ยวกับการทำงานร่วมกันของ Kotlin-Java อย่างเป็นทางการ

class Greeting @JvmOverloads constructor(
  loudness: Int = 5
) {
  @JvmOverloads
  fun sayHello(prefix: String = "Dr.", name: String) = // ...
}

อย่านำค่าพารามิเตอร์เริ่มต้นออก (Kotlin เท่านั้น)

หากเมธอดจัดส่งพร้อมพารามิเตอร์ที่มีค่าเริ่มต้น การนำค่าเริ่มต้นออกถือเป็นการเปลี่ยนแปลงที่ทำให้เกิดข้อขัดข้องในแหล่งที่มา

พารามิเตอร์ของวิธีการที่โดดเด่นและระบุตัวตนได้มากที่สุดควรอยู่ก่อน

หากมีเมธอดที่มีพารามิเตอร์หลายรายการ ให้ใส่พารามิเตอร์ที่เกี่ยวข้องมากที่สุดก่อน พารามิเตอร์ที่ระบุแฟล็กและตัวเลือกอื่นๆ มีความสำคัญน้อยกว่าพารามิเตอร์ที่อธิบายออบเจ็กต์ที่กำลังดำเนินการ หากมี การเรียกกลับเมื่อเสร็จสมบูรณ์ ให้วางไว้สุดท้าย

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);

ดูเพิ่มเติม: วางพารามิเตอร์ที่ไม่บังคับไว้ท้ายการโอเวอร์โหลด

ผู้สร้าง

เราขอแนะนำให้ใช้รูปแบบ Builder ในการสร้างออบเจ็กต์ Java ที่ซับซ้อน และมักใช้ใน Android สำหรับกรณีต่อไปนี้

  • พร็อพเพอร์ตี้ของออบเจ็กต์ผลลัพธ์ควรเปลี่ยนแปลงไม่ได้
  • มีพร็อพเพอร์ตี้ที่จำเป็นจำนวนมาก เช่น อาร์กิวเมนต์ของตัวสร้างหลายรายการ
  • พร็อพเพอร์ตี้มีความสัมพันธ์ที่ซับซ้อนในระหว่างการสร้าง เช่น ต้องมีขั้นตอนการยืนยัน โปรดทราบว่าระดับความซับซ้อนนี้ มักบ่งบอกถึงปัญหาเกี่ยวกับความสามารถในการใช้งานของ API

พิจารณาว่าคุณต้องการเครื่องมือสร้างหรือไม่ บิลเดอร์มีประโยชน์ในพื้นผิว API หากใช้เพื่อทำสิ่งต่อไปนี้

  • กำหนดค่าพารามิเตอร์การสร้างที่ไม่บังคับเพียงไม่กี่รายการจากชุดพารามิเตอร์จำนวนมากที่อาจมี
  • กำหนดค่าพารามิเตอร์การสร้างที่จำเป็นหรือไม่จำเป็นหลายรายการ ซึ่งบางครั้ง อาจเป็นประเภทที่คล้ายกันหรือตรงกัน ในกรณีที่เว็บไซต์การโทรอาจอ่าน ได้ยากหรือเขียนได้ง่าย
  • กำหนดค่าการสร้างออบเจ็กต์ทีละรายการ โดยโค้ดการกำหนดค่าที่แตกต่างกันหลายส่วนอาจเรียกใช้บิลเดอร์เป็นรายละเอียดการใช้งาน
  • อนุญาตให้ประเภทเติบโตโดยการเพิ่มพารามิเตอร์การสร้างเพิ่มเติมที่ไม่บังคับใน API เวอร์ชันในอนาคต

หากคุณมีประเภทที่มีพารามิเตอร์ที่จำเป็น 3 รายการหรือน้อยกว่า และไม่มีพารามิเตอร์ที่ไม่บังคับ คุณจะข้ามเครื่องมือสร้างและใช้ตัวสร้างธรรมดาได้เกือบทุกครั้ง

คลาสที่มาจาก Kotlin ควรใช้ตัวสร้างที่มีคำอธิบายประกอบ @JvmOverloads พร้อมอาร์กิวเมนต์เริ่มต้นมากกว่าตัวสร้าง แต่สามารถเลือกปรับปรุงความสามารถในการใช้งานสำหรับไคลเอ็นต์ Java ได้โดยการระบุตัวสร้างในกรณีที่ระบุไว้ก่อนหน้านี้ด้วย

class Tone @JvmOverloads constructor(
  val duration: Long = 1000,
  val frequency: Int = 2600,
  val dtmfConfigs: List<DtmfConfig> = emptyList()
) {
  class Builder {
    // ...
  }
}

คลาส Builder ต้องแสดงผล Builder

คลาส Builder ต้องเปิดใช้การเชื่อมโยงเมธอดโดยการส่งออบเจ็กต์ 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);
}

ต้องสร้างคลาส Builder ผ่านตัวสร้าง

เพื่อให้การสร้าง Builder สอดคล้องกันผ่านพื้นผิว Android API คุณต้องสร้าง Builder ทั้งหมดผ่านตัวสร้าง ไม่ใช่วิธีการสร้างแบบคงที่ สำหรับ API ที่ใช้ Kotlin Builder ต้องเป็นแบบสาธารณะแม้ว่าผู้ใช้ Kotlin จะคาดหวังให้ใช้ตัวสร้างโดยนัยผ่านกลไกการสร้างสไตล์เมธอด/DSL ของ Factory ก็ตาม ไลบรารีต้องไม่ใช้ @PublishedApi internal เพื่อ ซ่อนเครื่องมือสร้างคลาส Builder จากไคลเอ็นต์ Kotlin โดยเฉพาะ

public class Tone {
  public static Builder builder();
  public static class Builder {
  }
}
public class Tone {
  public static class Builder {
    public Builder();
  }
}

อาร์กิวเมนต์ทั้งหมดไปยังตัวสร้างของ Builder ต้องเป็นแบบบังคับ (เช่น @NonNull)

อาร์กิวเมนต์ที่ไม่บังคับ เช่น @Nullable ควรย้ายไปยังเมธอด Setter เครื่องมือสร้างควรส่ง NullPointerException (พิจารณาใช้ Objects.requireNonNull) หากไม่ได้ระบุอาร์กิวเมนต์ที่จำเป็น

คลาส Builder ควรเป็นคลาสภายในแบบคงที่สุดท้ายของประเภทที่สร้าง

เพื่อการจัดระเบียบอย่างมีตรรกะภายในแพ็กเกจ คลาส Builder ควร โดยทั่วไปจะแสดงเป็นคลาสภายในสุดท้ายของประเภทที่สร้างขึ้น เช่น Tone.Builder แทนที่จะเป็น ToneBuilder

บิลเดอร์อาจมีตัวสร้างเพื่อสร้างอินสแตนซ์ใหม่จากอินสแตนซ์ที่มีอยู่

Builder อาจมีตัวสร้างสำเนาเพื่อสร้างอินสแตนซ์ Builder ใหม่จาก Builder หรือออบเจ็กต์ที่สร้างแล้วที่มีอยู่ ไม่ควรระบุวิธีการอื่นในการสร้างอินสแตนซ์ของ Builder จาก Builder หรือออบเจ็กต์การสร้างที่มีอยู่

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);
  }
}
Setter ของ Builder ควรรับอาร์กิวเมนต์ @Nullable หาก Builder มีตัวสร้างสำเนา

การรีเซ็ตเป็นสิ่งจำเป็นหากอาจมีการสร้างอินสแตนซ์ใหม่ของ Builder จากอินสแตนซ์ที่มีอยู่ หากไม่มีตัวสร้างสำเนา ตัวสร้างอาจมีอาร์กิวเมนต์ @Nullable หรือ @NonNullable

public static class Builder {
  public Builder(Builder original);
  public Builder setObjectValue(@Nullable Object value);
}
ตัวตั้งค่าของ Builder อาจรับอาร์กิวเมนต์ @Nullable สำหรับพร็อพเพอร์ตี้ที่ไม่บังคับ

การใช้ค่าที่กำหนดให้เป็น Null ได้สำหรับอินพุตระดับที่ 2 มักจะง่ายกว่า โดยเฉพาะใน Kotlin ซึ่งใช้การอาร์กิวเมนต์เริ่มต้นแทนบิลเดอร์และการโอเวอร์โหลด

นอกจากนี้ @Nullable Setter จะจับคู่กับ 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();
}

การใช้งานทั่วไปใน Kotlin

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 สำหรับพร็อพเพอร์ตี้ที่เปลี่ยนแปลงได้ได้ในกรณีที่มีตัวตั้งค่าในคลาสที่สร้างขึ้น

หากชั้นเรียนมีพร็อพเพอร์ตี้ที่เปลี่ยนแปลงได้และต้องมีBuilder class ให้ถามตัวเองก่อนว่าชั้นเรียนควรมีพร็อพเพอร์ตี้ที่เปลี่ยนแปลงได้จริงๆ หรือไม่

จากนั้น หากคุณมั่นใจว่าต้องการพร็อพเพอร์ตี้ที่เปลี่ยนแปลงได้ ให้ตัดสินใจว่าสถานการณ์ต่อไปนี้สถานการณ์ใดเหมาะกับกรณีการใช้งานที่คุณคาดหวังไว้มากกว่า

  1. ออบเจ็กต์ที่สร้างขึ้นควรใช้งานได้ทันที ดังนั้นจึงควรระบุตัวตั้งค่าสำหรับพร็อพเพอร์ตี้ที่เกี่ยวข้องทั้งหมด ไม่ว่าจะเปลี่ยนแปลงได้หรือไม่ก็ตาม

    map.put(key, new Value.Builder(requiredValue)
        .setImmutableProperty(immutableValue)
        .setUsefulMutableProperty(usefulValue)
        .build());
    
  2. อาจต้องทำการเรียกเพิ่มเติมก่อนจึงจะใช้ออบเจ็กต์ที่สร้างขึ้นได้ ดังนั้นจึงไม่ควรระบุตัวตั้งค่าสำหรับพร็อพเพอร์ตี้ที่เปลี่ยนแปลงได้

    Value v = new Value.Builder(requiredValue)
        .setImmutableProperty(immutableValue)
        .build();
    v.setUsefulMutableProperty(usefulValue)
    Result r = v.performSomeAction();
    Key k = callSomeMethod(r);
    map.put(k, v);
    

อย่านำ 2 สถานการณ์นี้มาปะปนกัน

Value v = new Value.Builder(requiredValue)
    .setImmutableProperty(immutableValue)
    .setUsefulMutableProperty(usefulValue)
    .build();
Result r = v.performSomeAction();
Key k = callSomeMethod(r);
map.put(k, v);

Builder ไม่ควรมี Getter

Getter ควรอยู่ในออบเจ็กต์ที่สร้างขึ้น ไม่ใช่ใน Builder

ตัวตั้งค่าของ Builder ต้องมีตัวรับค่าที่สอดคล้องกันในคลาสที่สร้าง

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();
}

การตั้งชื่อเมธอด Builder

ชื่อเมธอดของ Builder ควรใช้รูปแบบ setFoo(), addFoo() หรือ clearFoo()

คลาส Builder คาดว่าจะประกาศเมธอด build()

คลาส Builder ควรประกาศเมธอด build() ที่แสดงผลอินสแตนซ์ของ ออบเจ็กต์ที่สร้างขึ้น

เมธอด build() ของ Builder ต้องแสดงผลออบเจ็กต์ @NonNull

คาดว่าเมธอด build() ของ Builder จะแสดงผลอินสแตนซ์ที่ไม่ใช่ Null ของออบเจ็กต์ที่สร้างขึ้น ในกรณีที่สร้างออบเจ็กต์ไม่ได้เนื่องจากพารามิเตอร์ไม่ถูกต้อง คุณสามารถเลื่อนการตรวจสอบไปยังเมธอดบิลด์และควรส่งIllegalStateException

อย่าเปิดเผยการล็อกภายใน

เมธอดใน API สาธารณะไม่ควรใช้คีย์เวิร์ด synchronized คีย์เวิร์ดนี้ ทําให้ระบบใช้ออบเจ็กต์หรือคลาสของคุณเป็นล็อก และเนื่องจากคีย์เวิร์ดนี้ แสดงต่อผู้อื่น คุณอาจพบผลข้างเคียงที่ไม่คาดคิดหากโค้ดอื่นๆ ภายนอกคลาสเริ่มใช้คีย์เวิร์ดนี้เพื่อวัตถุประสงค์ในการล็อก

แต่ให้ทำการล็อกที่จำเป็นกับออบเจ็กต์ภายในและส่วนตัวแทน

public synchronized void doThing() { ... }
private final Object mThingLock = new Object();

public void doThing() {
  synchronized (mThingLock) {
    ...
  }
}

เมธอดที่มีรูปแบบตัวเข้าถึงควรเป็นไปตามหลักเกณฑ์ของพร็อพเพอร์ตี้ Kotlin

เมื่อดูจากแหล่งที่มาของ Kotlin เมธอดที่มีรูปแบบการเข้าถึง ซึ่งใช้คำนำหน้า get, set หรือ is จะพร้อมใช้งานเป็นพร็อพเพอร์ตี้ Kotlin ด้วย เช่น int getField() ที่กำหนดไว้ใน Java จะพร้อมใช้งานใน Kotlin เป็นพร็อพเพอร์ตี้ val field: Int

ด้วยเหตุนี้ และเพื่อให้เป็นไปตามความคาดหวังของนักพัฒนาซอฟต์แวร์เกี่ยวกับลักษณะการทำงานของเมธอดตัวช่วยเข้าถึงโดยทั่วไป เมธอดที่ใช้คำนำหน้าเมธอดตัวช่วยเข้าถึงจึงควรมีลักษณะการทำงานคล้ายกับฟิลด์ Java หลีกเลี่ยงการใช้คำนำหน้าสไตล์ตัวเข้าถึงในกรณีต่อไปนี้

  • เมธอดมีผลข้างเคียง - ควรใช้ชื่อเมธอดที่สื่อความหมายมากขึ้น
  • วิธีการนี้เกี่ยวข้องกับงานที่ต้องใช้การคำนวณจำนวนมาก จึงควรใช้ compute
  • วิธีนี้เกี่ยวข้องกับการบล็อกหรืองานที่ใช้เวลานานอื่นๆ เพื่อส่งคืนค่า เช่น IPC หรือ I/O อื่นๆ -- fetch
  • เมธอดจะบล็อกเธรดจนกว่าจะแสดงผลค่าได้ ซึ่งควรใช้ await
  • เมธอดจะแสดงผลออบเจ็กต์อินสแตนซ์ใหม่ทุกครั้งที่เรียกใช้ ขอแนะนำให้ใช้ create
  • เมธอดอาจแสดงผลค่าไม่สำเร็จ -- โปรดใช้ request

โปรดทราบว่าการทำงานที่ต้องใช้การคำนวณสูงเพียงครั้งเดียวและแคชค่า สำหรับการเรียกใช้ครั้งต่อๆ ไปยังถือเป็นการทำงานที่ต้องใช้การคำนวณสูง ระบบจะไม่ตัดค่า Jank ออกจากเฟรม

ใช้ is เป็นคำนำหน้าสำหรับเมธอดตัวช่วยเข้าถึงแบบบูลีน

นี่คือรูปแบบการตั้งชื่อมาตรฐานสำหรับเมธอดและฟิลด์บูลีนใน Java โดยทั่วไปแล้ว ชื่อเมธอดและตัวแปรบูลีนควรเขียนเป็นคำถามที่ค่าที่ส่งคืนตอบ

เมธอดตัวช่วยเข้าถึงบูลีนของ Java ควรเป็นไปตามรูปแบบการตั้งชื่อ set/is และ ฟิลด์ควรใช้ is ดังนี้

// 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;

การใช้ set/is สำหรับเมธอดตัวเข้าถึง Java หรือ is สำหรับฟิลด์ Java จะช่วยให้ใช้เป็นพร็อพเพอร์ตี้จาก Kotlin ได้ดังนี้

obj.isVisible = true
obj.isFactoryResetProtectionEnabled = false
if (!obj.isAvailable) return

โดยทั่วไปแล้ว พร็อพเพอร์ตี้และเมธอดตัวช่วยควรใช้การตั้งชื่อเชิงบวก เช่น Enabled แทน Disabled การใช้คำศัพท์เชิงลบจะกลับความหมายของ true และ false และทำให้การให้เหตุผลเกี่ยวกับลักษณะการทำงานยากขึ้น

// Passing false here is a double-negative.
void setFactoryResetProtectionDisabled(boolean disabled);

ในกรณีที่บูลีนอธิบายการรวมหรือการเป็นเจ้าของพร็อพเพอร์ตี้ คุณอาจใช้ has แทน is ได้ แต่จะใช้กับไวยากรณ์พร็อพเพอร์ตี้ Kotlin ไม่ได้

// Transient state is an indirect property used to track state
// related to the object. The object is not transient; rather,
// the object "has" transient state associated with it:
void setHasTransientState(boolean hasTransientState);
boolean hasTransientState();

คำนำหน้าอื่นๆ ที่อาจเหมาะสมกว่า ได้แก่ สามารถและควร

// "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();

เมธอดที่สลับลักษณะการทำงานหรือฟีเจอร์อาจใช้คำนำหน้า is และคำต่อท้าย Enabled

// "Enabled" describes the availability of a property, and is
// more appropriate here than "can use" or "should use" the
// property:
void setWiFiRoamingSettingEnabled(boolean enabled)
boolean isWiFiRoamingSettingEnabled()

ในทำนองเดียวกัน วิธีการที่ระบุการขึ้นอยู่กับพฤติกรรมหรือฟีเจอร์อื่นๆ อาจใช้คำนำหน้า is และคำต่อท้าย Supported หรือ Required ดังนี้

// "Supported" describes whether this API would work on devices that support
// multiple users. The API "supports" multi-user:
void setMultiUserSupported(boolean supported)
boolean isMultiUserSupported()
// "Required" describes whether this API depends on devices that support
// multiple users. The API "requires" multi-user:
void setMultiUserRequired(boolean required)
boolean isMultiUserRequired()

โดยทั่วไปแล้ว ชื่อเมธอดควรเขียนเป็นคำถามที่ค่าที่ส่งคืนตอบ

เมธอดพร็อพเพอร์ตี้ Kotlin

สำหรับพร็อพเพอร์ตี้คลาส var foo: Foo Kotlin จะสร้างเมธอด get/set โดยใช้กฎที่สอดคล้องกัน ได้แก่ นำหน้าด้วย get และเปลี่ยนอักขระตัวแรกเป็นตัวพิมพ์ใหญ่สำหรับ ตัวรับค่า และนำหน้าด้วย set และเปลี่ยนอักขระตัวแรกเป็นตัวพิมพ์ใหญ่สำหรับตัวตั้งค่า การประกาศพร็อพเพอร์ตี้ จะสร้างเมธอดที่ชื่อ public Foo getFoo() และ public void setFoo(Foo foo) ตามลำดับ

หากพร็อพเพอร์ตี้เป็นประเภท Boolean จะมีกฎเพิ่มเติมในการสร้างชื่อ หากชื่อพร็อพเพอร์ตี้ขึ้นต้นด้วย is จะไม่มีการนำหน้า get สำหรับชื่อเมธอด Getter แต่จะใช้ชื่อพร็อพเพอร์ตี้เป็น Getter แทน ดังนั้น ควรตั้งชื่อพร็อพเพอร์ตี้ Boolean โดยมีคำนำหน้า is ตามลำดับ เพื่อให้เป็นไปตามหลักเกณฑ์การตั้งชื่อ

var isVisible: Boolean

หากพร็อพเพอร์ตี้ของคุณเป็นหนึ่งในข้อยกเว้นที่กล่าวถึงข้างต้นและขึ้นต้นด้วยคำนำหน้าที่เหมาะสม ให้ใช้คำอธิบายประกอบ @get:JvmName ในพร็อพเพอร์ตี้เพื่อระบุชื่อที่เหมาะสมด้วยตนเอง

@get:JvmName("hasTransientState")
var hasTransientState: Boolean

@get:JvmName("canRecord")
var canRecord: Boolean

@get:JvmName("shouldFitWidth")
var shouldFitWidth: Boolean

ตัวช่วยเข้าถึงบิตมาสก์

ดูหลักเกณฑ์ API เกี่ยวกับการกำหนดแฟล็กบิตมาสก์ได้ที่ใช้ @IntDef สำหรับแฟล็กบิตมาสก์

เซ็ตเตอร์

ควรมีเมธอด Setter 2 รายการ ได้แก่ เมธอดที่รับสตริงบิตแบบเต็มและเขียนทับแฟล็กที่มีอยู่ทั้งหมด และอีกเมธอดที่รับบิตมาสก์ที่กำหนดเองเพื่อให้มีความยืดหยุ่นมากขึ้น

/**
 * 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);

Getter

ควรระบุตัวรับค่า 1 รายการเพื่อรับบิตแมสก์แบบเต็ม

/**
 * 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();

ใช้สาธารณะแทนที่ได้รับการป้องกัน

ใช้ public ก่อน protected เสมอใน API สาธารณะ การเข้าถึงที่ได้รับการป้องกันจะสร้างความเจ็บปวดในระยะยาว เนื่องจากผู้ใช้ต้องลบล้างเพื่อจัดเตรียมตัวเข้าถึงสาธารณะในกรณีที่การเข้าถึงภายนอกโดยค่าเริ่มต้นจะดีพอๆ กัน

โปรดทราบว่าprotectedการมองเห็นไม่ได้ป้องกันไม่ให้นักพัฒนาแอปเรียกใช้ API แต่จะทำให้การเรียกใช้ดูน่ารำคาญมากขึ้นเล็กน้อยเท่านั้น

อย่าใช้ทั้ง equals() และ hashCode() หรือใช้ทั้ง 2 อย่าง

หากลบล้างรายการใดรายการหนึ่ง คุณต้องลบล้างอีกรายการด้วย

ใช้ toString() สำหรับคลาสข้อมูล

เราขอแนะนำให้คลาสข้อมูลลบล้าง toString() เพื่อช่วยให้นักพัฒนาซอฟต์แวร์แก้ไขข้อบกพร่องของโค้ด ได้

ระบุว่าเอาต์พุตมีไว้สำหรับลักษณะการทำงานของโปรแกรมหรือการแก้ไขข้อบกพร่อง

เลือกว่าต้องการให้ลักษณะการทำงานของโปรแกรมขึ้นอยู่กับการติดตั้งใช้งานของคุณหรือไม่ เช่น UUID.toString() และ File.toString() จะบันทึกรูปแบบเฉพาะของตนเองเพื่อให้โปรแกรมใช้งาน หากคุณแสดง ข้อมูลเพื่อการแก้ไขข้อบกพร่องเท่านั้น เช่น Intent ให้ใช้เอกสารจากคลาสแม่

อย่าใส่ข้อมูลเพิ่มเติม

ข้อมูลทั้งหมดที่ได้จาก toString() ควรพร้อมใช้งานผ่าน API สาธารณะของออบเจ็กต์ด้วย ไม่เช่นนั้น คุณจะสนับสนุนให้นักพัฒนาซอฟต์แวร์แยกวิเคราะห์ และใช้toString()เอาต์พุตของคุณ ซึ่งจะป้องกันการเปลี่ยนแปลงในอนาคต แนวทางปฏิบัติที่ดี คือการใช้ toString() โดยใช้เฉพาะ API สาธารณะของออบเจ็กต์

ไม่แนะนำให้พึ่งพาเอาต์พุตการแก้ไขข้อบกพร่อง

แม้ว่าจะเป็นไปไม่ได้ที่จะป้องกันไม่ให้นักพัฒนาซอฟต์แวร์ใช้เอาต์พุตการแก้ไขข้อบกพร่อง รวมถึงSystem.identityHashCodeของออบเจ็กต์ในtoString() เอาต์พุต แต่การดำเนินการดังกล่าวจะช่วยลดโอกาสที่ออบเจ็กต์ 2 รายการจะให้เอาต์พุตtoString()ที่เท่ากัน

@Override
public String toString() {
  return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + " {mFoo=" + mFoo + "}";
}

ซึ่งจะช่วยป้องกันไม่ให้นักพัฒนาแอปเขียนการยืนยันการทดสอบ เช่น assertThat(a.toString()).isEqualTo(b.toString()) ในออบเจ็กต์ของคุณได้อย่างมีประสิทธิภาพ

ใช้ createFoo เมื่อส่งคืนออบเจ็กต์ที่สร้างขึ้นใหม่

ใช้คำนำหน้า create ไม่ใช่ get หรือ new สำหรับเมธอดที่จะสร้างค่าที่ส่งคืน เช่น โดยการสร้างออบเจ็กต์ใหม่

เมื่อเมธอดจะสร้างออบเจ็กต์เพื่อส่งคืน ให้ระบุอย่างชัดเจนในชื่อเมธอด

public FooThing getFooThing() {
  return new FooThing();
}
public FooThing createFooThing() {
  return new FooThing();
}

เมธอดที่ยอมรับออบเจ็กต์ File ควรยอมรับสตรีมด้วย

ตำแหน่งที่จัดเก็บข้อมูลใน Android ไม่ได้อยู่ในไฟล์บนดิสก์เสมอไป เช่น เนื้อหาที่ส่งผ่านขอบเขตของผู้ใช้จะแสดงเป็น content:// Uri หากต้องการเปิดใช้การประมวลผลแหล่งข้อมูลต่างๆ API ที่ยอมรับออบเจ็กต์ File ควรยอมรับ InputStream, OutputStream หรือทั้ง 2 อย่างด้วย

public void setDataSource(File file)
public void setDataSource(InputStream stream)

ใช้และส่งคืนข้อมูลดั้งเดิมแทนเวอร์ชันที่อยู่ในกล่อง

หากต้องการสื่อสารค่าที่ขาดหายไปหรือค่าว่าง ให้ลองใช้ -1 Integer.MAX_VALUE หรือ Integer.MIN_VALUE

public java.lang.Integer getLength()
public void setLength(java.lang.Integer)
public int getLength()
public void setLength(int value)

การหลีกเลี่ยงคลาสที่เทียบเท่ากับประเภทข้อมูลพื้นฐานจะช่วยหลีกเลี่ยงค่าใช้จ่ายด้านหน่วยความจำของ คลาสเหล่านี้ การเข้าถึงค่าของเมธอด และที่สำคัญกว่านั้นคือการแปลงอัตโนมัติที่ มาจากการแคสต์ระหว่างประเภทข้อมูลพื้นฐานและประเภทออบเจ็กต์ การหลีกเลี่ยงพฤติกรรมเหล่านี้ จะช่วยประหยัดหน่วยความจำและการจัดสรรชั่วคราว ซึ่งอาจทำให้เกิดการเก็บขยะที่ใช้ทรัพยากรมากและบ่อยขึ้น

ใช้คำอธิบายประกอบเพื่อชี้แจงพารามิเตอร์และค่าที่ส่งคืนที่ถูกต้อง

เราได้เพิ่มคำอธิบายประกอบสำหรับนักพัฒนาแอปเพื่อช่วยชี้แจงค่าที่อนุญาตในสถานการณ์ต่างๆ ซึ่งจะช่วยให้เครื่องมือต่างๆ ช่วยนักพัฒนาแอปได้ง่ายขึ้นเมื่อระบุค่าที่ไม่ถูกต้อง (เช่น การส่ง int โดยพลการเมื่อเฟรมเวิร์กต้องการค่าคงที่ค่าใดค่าหนึ่งจากชุดค่าคงที่ที่เฉพาะเจาะจง) ใช้คำอธิบายประกอบต่อไปนี้เมื่อเหมาะสม

ความสามารถในการเว้นว่าง

Java API ต้องมี Nullability Annotation ที่ชัดเจน แต่แนวคิดเรื่อง Nullability เป็นส่วนหนึ่งของภาษา Kotlin และไม่ควรใช้ Nullability Annotation ใน Kotlin API

@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. การทำเครื่องหมายสิ่งต่างๆ เป็น @Nullable เป็นฟีเจอร์ใหม่ใน Android ดังนั้นวิธีการ API ของ Android ส่วนใหญ่จึงไม่มีเอกสารประกอบที่สอดคล้องกัน ดังนั้นเราจึงมีสถานะ 3 สถานะคือ "ไม่ทราบ @Nullable @NonNull" ซึ่งเป็นเหตุผลที่@NonNull เป็นส่วนหนึ่ง ของหลักเกณฑ์ API

@NonNull
public String getName()

public void setName(@NonNull String name)

สำหรับเอกสารแพลตฟอร์ม Android การใส่คำอธิบายประกอบพารามิเตอร์ของเมธอดจะสร้างเอกสารประกอบในรูปแบบ "ค่านี้อาจเป็น Null" โดยอัตโนมัติ เว้นแต่จะมีการใช้ "null" อย่างชัดเจนในเอกสารประกอบพารามิเตอร์ที่อื่น

เมธอด "ไม่เป็น Null จริงๆ" ที่มีอยู่: เมธอดที่มีอยู่ใน API ที่ไม่มี การประกาศคำอธิบายประกอบ @Nullable อาจมีคำอธิบายประกอบ @Nullable หากเมธอดสามารถ แสดงผล null ภายใต้สถานการณ์ที่เฉพาะเจาะจงและชัดเจน (เช่น findViewById()) ควรเพิ่มเมธอด @NotNull requireFoo() ที่เกี่ยวข้องซึ่งส่ง IllegalArgumentException สำหรับนักพัฒนาแอปที่ไม่ต้องการตรวจสอบค่า Null

วิธีการอินเทอร์เฟซ: API ใหม่ควรเพิ่มคำอธิบายประกอบที่เหมาะสมเมื่อ ใช้วิธีการอินเทอร์เฟซ เช่น Parcelable.writeToParcel() (กล่าวคือ วิธีการในคลาสที่ใช้ควรเป็น writeToParcel(@NonNull Parcel, int) ไม่ใช่ writeToParcel(Parcel, int)) แต่ API ที่มีอยู่ซึ่งไม่มีคำอธิบายประกอบไม่จำเป็นต้อง "แก้ไข"

การบังคับใช้ความสามารถในการเว้นว่าง

ใน Java เราขอแนะนำให้ใช้วิธีการตรวจสอบอินพุตสำหรับ@NonNull พารามิเตอร์โดยใช้ Objects.requireNonNull() และส่ง NullPointerException เมื่อพารามิเตอร์เป็น Null ซึ่งจะ ดำเนินการโดยอัตโนมัติใน Kotlin

แหล่งข้อมูล

ตัวระบุทรัพยากร: พารามิเตอร์จำนวนเต็มที่ระบุรหัสสำหรับทรัพยากรที่เฉพาะเจาะจงควรมีคำอธิบายประกอบพร้อมคำจำกัดความประเภททรัพยากรที่เหมาะสม มีคำอธิบายประกอบสำหรับทรัพยากรทุกประเภท เช่น @StringRes, @ColorRes และ @AnimRes นอกเหนือจาก @AnyRes ที่ครอบคลุมทุกอย่าง เช่น

public void setTitle(@StringRes int resId)

@IntDef สำหรับชุดค่าคงที่

ค่าคงที่พิเศษ: พารามิเตอร์ String และ int ที่มีไว้เพื่อรับค่าใดค่าหนึ่งจากชุดค่าที่เป็นไปได้แบบจำกัดซึ่งระบุโดยค่าคงที่สาธารณะควรมีคำอธิบายประกอบที่เหมาะสมด้วย @StringDef หรือ @IntDef คำอธิบายประกอบเหล่านี้ช่วยให้ คุณสร้างคำอธิบายประกอบใหม่ที่ใช้ได้ซึ่งทำงานเหมือน typedef สำหรับ พารามิเตอร์ที่อนุญาต เช่น

/** @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);

แนะนําให้ใช้วิธีการตรวจสอบความถูกต้องของพารามิเตอร์ที่อธิบายประกอบ และแสดง IllegalArgumentException หากพารามิเตอร์ไม่ได้เป็นส่วนหนึ่งของ @IntDef

@IntDef สำหรับแฟล็กบิตมาสก์

คำอธิบายประกอบยังระบุได้ด้วยว่าค่าคงที่เป็นแฟล็ก และสามารถ รวมกับ & และ 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 สำหรับชุดค่าคงที่สตริง

นอกจากนี้ยังมีคำอธิบายประกอบ @StringDef ซึ่งเหมือนกับ @IntDef ในส่วนก่อนหน้าทุกประการ แต่ใช้สำหรับค่าคงที่ String คุณสามารถใส่ค่า "prefix" หลายค่าซึ่งใช้เพื่อสร้างเอกสารประกอบสำหรับค่าทั้งหมดโดยอัตโนมัติ

@SdkConstant สำหรับค่าคงที่ของ SDK

@SdkConstant ใส่คำอธิบายประกอบฟิลด์สาธารณะเมื่อเป็นค่าใดค่าหนึ่งต่อไปนี้ SdkConstant 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";

ระบุค่า Null ที่เข้ากันได้สำหรับการลบล้าง

สำหรับการใช้งานร่วมกับ API ความสามารถในการเป็นค่าว่างของการลบล้างควรเข้ากันได้กับ ความสามารถในการเป็นค่าว่างปัจจุบันขององค์ประกอบหลัก ตารางต่อไปนี้แสดง ความคาดหวังด้านความเข้ากันได้ กล่าวอย่างตรงไปตรงมาคือ การลบล้างควรมีข้อจำกัดเท่ากันหรือมากกว่าองค์ประกอบที่ลบล้างเท่านั้น

ประเภท ผู้ปกครอง บุตร
ประเภทการแสดงผล ไม่ได้ใส่คำอธิบายประกอบ ไม่ได้ใส่คำอธิบายประกอบหรือไม่ใช่ค่าว่าง
ประเภทการแสดงผล เว้นว่างได้ เป็น Null ได้หรือไม่
ประเภทการแสดงผล Nonnull Nonnull
อาร์กิวเมนต์ที่สนุก ไม่ได้ใส่คำอธิบายประกอบ ไม่ได้ใส่คำอธิบายประกอบหรือเป็น Null ได้
อาร์กิวเมนต์ที่สนุก เว้นว่างได้ เว้นว่างได้
อาร์กิวเมนต์ที่สนุก Nonnull เป็น Null ได้หรือไม่

ใช้อาร์กิวเมนต์ที่กำหนดให้ต้องมีค่า (เช่น @NonNull) หากเป็นไปได้

เมื่อมีการโอเวอร์โหลดเมธอด ให้กำหนดอาร์กิวเมนต์ทั้งหมดเป็นแบบไม่เป็น Null

public void startActivity(@NonNull Component component) { ... }
public void startActivity(@NonNull Component component, @NonNull Bundle options) { ... }

กฎนี้ใช้กับตัวตั้งค่าพร็อพเพอร์ตี้ที่โอเวอร์โหลดด้วย อาร์กิวเมนต์หลัก ต้องไม่เป็น Null และควรใช้การล้างพร็อพเพอร์ตี้เป็นเมธอดแยก ซึ่งจะป้องกันการเรียกที่ "ไร้สาระ" ซึ่งนักพัฒนาแอปต้องตั้งค่าพารามิเตอร์ต่อท้ายแม้ว่าจะไม่จำเป็นก็ตาม

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()

ใช้ประเภทการคืนค่าที่กำหนดให้ไม่เป็น Null (เช่น @NonNull) สำหรับคอนเทนเนอร์

สำหรับประเภทคอนเทนเนอร์ เช่น Bundle หรือ Collection ให้ส่งคืนคอนเทนเนอร์ที่ว่างเปล่าและเปลี่ยนแปลงไม่ได้ (หากมี) ในกรณีที่ใช้ null เพื่อ แยกความแตกต่างของความพร้อมจำหน่ายคอนเทนเนอร์ ให้พิจารณาใช้วิธีบูลีนแยกต่างหาก

@NonNull
public Bundle getExtras() { ... }

คำอธิบายประกอบการยอมรับค่าว่างสำหรับคู่ get และ set ต้องตรงกัน

คู่เมธอด Get และ Set สำหรับพร็อพเพอร์ตี้เชิงตรรกะเดียวควรสอดคล้องกันเสมอใน คำอธิบายประกอบการยอมรับค่า Null การไม่ปฏิบัติตามหลักเกณฑ์นี้จะทำให้ไวยากรณ์พร็อพเพอร์ตี้ของ Kotlin ไม่ทำงาน และการเพิ่มคำอธิบายประกอบเกี่ยวกับค่า Null ที่ไม่เห็นด้วยลงใน เมธอดพร็อพเพอร์ตี้ที่มีอยู่จึงเป็นการเปลี่ยนแปลงที่ทำให้แหล่งข้อมูลเสียหายสำหรับผู้ใช้ Kotlin

@NonNull
public Bundle getExtras() { ... }
public void setExtras(@NonNull Bundle bundle) { ... }

ค่าที่แสดงผลในกรณีที่เกิดข้อผิดพลาดหรือล้มเหลว

API ทั้งหมดควรอนุญาตให้แอปตอบสนองต่อข้อผิดพลาดได้ การแสดงผล false, -1, null หรือค่าอื่นๆ ที่ครอบคลุมทั้งหมดของ "เกิดข้อผิดพลาด" ไม่ได้บอกนักพัฒนาแอป มากพอเกี่ยวกับความล้มเหลวในการกำหนดความคาดหวังของผู้ใช้หรือติดตาม ความน่าเชื่อถือของแอปในภาคสนามอย่างถูกต้อง เมื่อออกแบบ API ให้ลองนึกว่าคุณกำลังสร้างแอป หากพบข้อผิดพลาด API ให้ข้อมูลเพียงพอที่จะแสดงต่อผู้ใช้หรือตอบสนองอย่างเหมาะสมหรือไม่

  1. คุณสามารถ (และเราขอแนะนำ) ให้ใส่ข้อมูลโดยละเอียดในข้อความข้อยกเว้น แต่ไม่ควรให้นักพัฒนาแอปต้องแยกวิเคราะห์ข้อความดังกล่าวเพื่อจัดการข้อผิดพลาด อย่างเหมาะสม ควรแสดงรหัสข้อผิดพลาดแบบละเอียดหรือข้อมูลอื่นๆ เป็น เมธอด
  2. ตรวจสอบว่าตัวเลือกการจัดการข้อผิดพลาดที่คุณเลือกช่วยให้คุณมีความยืดหยุ่นในการ เปิดตัวข้อผิดพลาดประเภทใหม่ๆ ในอนาคต สำหรับ @IntDef นั่นหมายถึงการรวมค่า OTHER หรือ UNKNOWN เมื่อแสดงรหัสใหม่ คุณสามารถตรวจสอบ targetSdkVersion ของผู้เรียกเพื่อหลีกเลี่ยงการแสดงรหัสข้อผิดพลาดที่แอปไม่รู้จัก สำหรับข้อยกเว้น ให้มีคลาสหลักทั่วไปที่ข้อยกเว้น ใช้ เพื่อให้โค้ดที่จัดการประเภทนั้นๆ สามารถตรวจหาและ จัดการประเภทย่อยได้ด้วย
  3. นักพัฒนาแอปควรจะละเลยข้อผิดพลาดโดยไม่ตั้งใจได้ยากหรือเป็นไปไม่ได้ หากข้อผิดพลาดของคุณสื่อสารด้วยการแสดงผลค่า ให้ใส่คำอธิบายประกอบเมธอดด้วย @CheckResult

ควรใช้ ? extends RuntimeException เมื่อถึงเงื่อนไขความล้มเหลวหรือข้อผิดพลาด เนื่องจากสิ่งที่นักพัฒนาซอฟต์แวร์ทำผิดพลาด เช่น การละเลย ข้อจำกัดเกี่ยวกับพารามิเตอร์อินพุตหรือการตรวจสอบสถานะที่สังเกตได้ไม่สำเร็จ

เมธอด Setter หรือเมธอดการดำเนินการ (เช่น perform) อาจแสดงรหัสสถานะจำนวนเต็ม หากการดำเนินการอาจล้มเหลวเนื่องจากสถานะที่อัปเดตแบบไม่พร้อมกันหรือ เงื่อนไขที่อยู่นอกเหนือการควบคุมของนักพัฒนาแอป

ควรกำหนดรหัสสถานะในคลาสที่ประกอบด้วยเป็นpublic static final ฟิลด์ โดยมีคำนำหน้าเป็น ERROR_ และแสดงรายการในคำอธิบายประกอบ @hide @IntDef

ชื่อเมธอดควรขึ้นต้นด้วยคำกริยาเสมอ ไม่ใช่คำนาม

ชื่อของเมธอดควรเริ่มต้นด้วยคำกริยาเสมอ (เช่น get, create, reload ฯลฯ) ไม่ใช่ชื่อออบเจ็กต์ที่คุณดำเนินการ

public void tableReload() {
  mTable.reload();
}
public void reloadTable() {
  mTable.reload();
}

ใช้ประเภทคอลเล็กชันแทนอาร์เรย์เป็นประเภทการคืนค่าหรือพารามิเตอร์

อินเทอร์เฟซคอลเล็กชันที่พิมพ์แบบทั่วไปมีข้อดีหลายประการเหนืออาร์เรย์ รวมถึงสัญญา API ที่แข็งแกร่งยิ่งขึ้นเกี่ยวกับการเรียงลำดับและความเป็นเอกลักษณ์ การรองรับ Generics และวิธีการอำนวยความสะดวกที่เป็นมิตรกับนักพัฒนาแอปจำนวนมาก

ข้อยกเว้นสำหรับ Primitive

หากองค์ประกอบเป็นแบบดั้งเดิม ให้ใช้อาร์เรย์แทนเพื่อหลีกเลี่ยง ค่าใช้จ่ายในการบ็อกซ์อัตโนมัติ ดูหัวข้อ ใช้และส่งคืนค่าดั้งเดิมแทนเวอร์ชันที่อยู่ในกล่อง

ข้อยกเว้นสำหรับโค้ดที่ไวต่อประสิทธิภาพ

ในบางสถานการณ์ที่ใช้ API ในโค้ดที่คำนึงถึงประสิทธิภาพ (เช่น กราฟิกหรือ API อื่นๆ สำหรับการวัด/เลย์เอาต์/การวาด) คุณสามารถใช้อาร์เรย์แทนคอลเล็กชันเพื่อลดการจัดสรรและการเปลี่ยนแปลงหน่วยความจำ

ข้อยกเว้นสำหรับ Kotlin

อาร์เรย์ Kotlin จะไม่เปลี่ยนแปลง และภาษา Kotlin มี API ยูทิลิตีมากมาย เกี่ยวกับอาร์เรย์ ดังนั้นอาร์เรย์จึงเทียบเท่ากับ List และ Collection สำหรับ Kotlin API ที่ตั้งใจให้เข้าถึงจาก Kotlin

เลือกใช้คอลเล็กชัน @NonNull

แนะนำให้ใช้ @NonNull สำหรับออบเจ็กต์คอลเล็กชันเสมอ เมื่อแสดงผลคอลเล็กชันที่ว่างเปล่า ให้ใช้Collections.emptyเมธอดที่เหมาะสมเพื่อแสดงผลออบเจ็กต์คอลเล็กชันที่มีต้นทุนต่ำ พิมพ์อย่างถูกต้อง และเปลี่ยนแปลงไม่ได้

ในกรณีที่รองรับการอธิบายประกอบประเภท ให้ใช้ @NonNull สำหรับองค์ประกอบของคอลเล็กชันเสมอ

นอกจากนี้ คุณควรใช้ @NonNull เมื่อใช้อาร์เรย์แทนคอลเล็กชัน (ดูรายการก่อนหน้า) หากคุณกังวลเรื่องการจัดสรรออบเจ็กต์ ให้สร้างค่าคงที่แล้วส่งต่อค่าคงที่นั้น เนื่องจากอาร์เรย์ว่างเป็นแบบเปลี่ยนแปลงไม่ได้ ตัวอย่าง

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;
}

ความสามารถในการเปลี่ยนแปลงของคอลเล็กชัน

API ของ Kotlin ควรใช้ประเภทการคืนค่าแบบอ่านอย่างเดียว (ไม่ใช่ Mutable) สำหรับคอลเล็กชัน โดยค่าเริ่มต้น เว้นแต่สัญญา API จะกำหนดให้ใช้ประเภทการคืนค่าที่เปลี่ยนแปลงได้โดยเฉพาะ

อย่างไรก็ตาม Java API ควรใช้ประเภทการคืนค่าที่เปลี่ยนแปลงได้โดยค่าเริ่มต้น เนื่องจาก การติดตั้งใช้งาน Java API ของแพลตฟอร์ม Android ยังไม่มีการติดตั้งใช้งานที่สะดวก สำหรับคอลเล็กชันที่เปลี่ยนแปลงไม่ได้ ข้อยกเว้นสำหรับกฎข้อนี้คือCollections.emptyประเภทการคืนสินค้า ซึ่งเปลี่ยนแปลงไม่ได้ ในกรณีที่ไคลเอ็นต์อาจใช้ประโยชน์จากความสามารถในการเปลี่ยนแปลง โดยตั้งใจหรือโดยไม่ตั้งใจเพื่อทำลายรูปแบบการใช้งานที่ตั้งใจไว้ของ API API ของ Java ควรพิจารณาอย่างยิ่งที่จะส่งคืนสำเนาแบบตื้นของคอลเล็กชัน

@Nullable
public PermissionInfo[] getGrantedPermissions() {
  return mPermissions;
}
@NonNull
public Set<PermissionInfo> getGrantedPermissions() {
  if (mPermissions == null) {
    return Collections.emptySet();
  }
  return new ArraySet<>(mPermissions);
}

ประเภทการคืนค่าที่เปลี่ยนแปลงได้โดยชัดแจ้ง

API ที่แสดงผลคอลเล็กชันไม่ควรแก้ไขออบเจ็กต์คอลเล็กชันที่แสดงผลหลังจากแสดงผล หากคอลเล็กชันที่ส่งคืนต้องมีการเปลี่ยนแปลงหรือนำกลับมาใช้ใหม่ใน บางลักษณะ เช่น มุมมองที่ดัดแปลงของชุดข้อมูลที่เปลี่ยนแปลงได้ จะต้องมีการบันทึกพฤติกรรมที่แน่นอนของเมื่อเนื้อหาเปลี่ยนแปลงได้ไว้อย่างชัดเจน หรือ เป็นไปตามรูปแบบการตั้งชื่อ API ที่กำหนดไว้

/**
 * Returns a view of this object as a list of [Item]s.
 */
fun MyObject.asList(): List<Item> = MyObjectListWrapper(this)

อธิบายถึงรูปแบบของ Kotlin .asFoo() ด้านล่างและอนุญาตให้คอลเล็กชันที่ .asList() ส่งคืนเปลี่ยนแปลงได้หากคอลเล็กชันเดิมเปลี่ยนแปลง

ความสามารถในการเปลี่ยนแปลงของออบเจ็กต์ประเภทข้อมูลที่แสดงผล

API ที่แสดงผลออบเจ็กต์ประเภทข้อมูล ไม่ควรแก้ไขพร็อพเพอร์ตี้ของออบเจ็กต์ที่แสดงผลหลังจากแสดงผลแล้ว ซึ่งคล้ายกับ API ที่แสดงผลคอลเล็กชัน

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)
}

ในกรณีที่น้อยมาก โค้ดบางรายการที่ไวต่อประสิทธิภาพอาจได้รับประโยชน์จากการจัดกลุ่มหรือการนำออบเจ็กต์กลับมาใช้ใหม่ อย่าเขียนโครงสร้างข้อมูลของพูลออบเจ็กต์ด้วยตนเองและอย่าเปิดเผยออบเจ็กต์ที่นำกลับมาใช้ใหม่ใน API สาธารณะ ไม่ว่าจะเป็นกรณีใดก็ตาม โปรดระมัดระวังอย่างยิ่ง เกี่ยวกับการจัดการการเข้าถึงพร้อมกัน

การใช้ประเภทพารามิเตอร์ vararg

เราขอแนะนำให้ใช้ทั้ง Kotlin และ Java API ในกรณีที่นักพัฒนาซอฟต์แวร์น่าจะสร้างอาร์เรย์ที่เว็บไซต์ที่เรียกใช้เพื่อจุดประสงค์เดียวในการส่งพารามิเตอร์ที่เกี่ยวข้องหลายรายการซึ่งมีประเภทเดียวกันvararg

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);

สำเนาเพื่อการป้องกัน

การติดตั้งใช้งานพารามิเตอร์ vararg ทั้งใน Java และ Kotlin จะคอมไพล์เป็นไบต์โค้ดที่อิงตามอาร์เรย์เดียวกัน และด้วยเหตุนี้จึงอาจเรียกใช้จากโค้ด Java ด้วยอาร์เรย์ที่เปลี่ยนแปลงได้ เราขอแนะนำเป็นอย่างยิ่งให้ผู้ออกแบบ API สร้างสำเนาตื้นๆ ที่ป้องกันได้ ของพารามิเตอร์อาร์เรย์ในกรณีที่จะมีการบันทึกพารามิเตอร์ดังกล่าวไปยัง ฟิลด์หรือคลาสภายในที่ไม่ระบุชื่อ

public void setValues(SomeObject... values) {
   this.values = Arrays.copyOf(values, values.length);
}

โปรดทราบว่าการสร้างสำเนาเพื่อป้องกันไม่ได้ให้การป้องกันใดๆ ต่อการ แก้ไขพร้อมกันระหว่างการเรียกใช้เมธอดครั้งแรกและการสร้างสำเนา ทั้งยังไม่ได้ป้องกันการเปลี่ยนแปลงของออบเจ็กต์ที่อยู่ในอาร์เรย์

ระบุความหมายที่ถูกต้องด้วยพารามิเตอร์ประเภทคอลเล็กชันหรือประเภทที่ส่งคืน

List<Foo> เป็นตัวเลือกเริ่มต้น แต่ลองใช้ประเภทอื่นๆ เพื่อให้มีความหมายเพิ่มเติม

  • ใช้ Set<Foo> หาก API ไม่สนใจลำดับขององค์ประกอบและไม่อนุญาตให้มีรายการที่ซ้ำกันหรือรายการที่ซ้ำกันไม่มีความหมาย

  • Collection<Foo>, หาก API ไม่สนใจลำดับและอนุญาต รายการที่ซ้ำกัน

ฟังก์ชันการแปลง Kotlin

Kotlin มักใช้ .toFoo() และ .asFoo() เพื่อรับออบเจ็กต์ประเภทอื่นจากออบเจ็กต์ที่มีอยู่ โดย Foo คือชื่อของประเภทการคืนค่าของ Conversion ซึ่งสอดคล้องกับ JDK Object.toString() ที่คุ้นเคย Kotlin ใช้แนวคิดนี้ไปอีกขั้นโดยใช้กับ Conversion แบบดั้งเดิม เช่น 25.toFloat()

ความแตกต่างระหว่าง Conversion ที่ชื่อ .toFoo() กับ .asFoo() มีความสําคัญดังนี้

ใช้ .toFoo() เมื่อสร้างออบเจ็กต์ใหม่ที่เป็นอิสระ

เช่นเดียวกับ .toString() Conversion "to" จะแสดงผลออบเจ็กต์ใหม่ที่เป็นอิสระ หากมีการแก้ไขออบเจ็กต์ต้นฉบับในภายหลัง ออบเจ็กต์ใหม่จะไม่แสดงการเปลี่ยนแปลงเหล่านั้น ในทำนองเดียวกัน หากมีการแก้ไขออบเจ็กต์ใหม่ในภายหลัง ออบเจ็กต์เก่าจะไม่แสดงการเปลี่ยนแปลงเหล่านั้น

fun Foo.toBundle(): Bundle = Bundle().apply {
    putInt(FOO_VALUE_KEY, value)
}

ใช้ .asFoo() เมื่อสร้าง Wrapper ที่ขึ้นต่อกัน ออบเจ็กต์ที่ตกแต่ง หรือการแคสต์

การแคสต์ใน Kotlin จะดำเนินการโดยใช้คีย์เวิร์ด as โดยเป็นการเปลี่ยนแปลงอินเทอร์เฟซ แต่ไม่ใช่การเปลี่ยนแปลงตัวตน เมื่อใช้เป็นคำนำหน้าในฟังก์ชันส่วนขยาย .asFoo() จะตกแต่งผู้รับ การเปลี่ยนแปลงในออบเจ็กต์ผู้รับเดิมจะแสดงในออบเจ็กต์ที่ asFoo() ส่งคืน การเปลี่ยนแปลงในFooออบเจ็กต์ใหม่อาจแสดงในออบเจ็กต์เดิม

fun <T> Flow<T>.asLiveData(): LiveData<T> = liveData {
    collect {
        emit(it)
    }
}

ควรเขียนฟังก์ชัน Conversion เป็นฟังก์ชันส่วนขยาย

การเขียนฟังก์ชัน Conversion นอกคำจำกัดความของทั้งคลาสตัวรับและคลาสผลลัพธ์ จะช่วยลดการเชื่อมโยงระหว่างประเภทต่างๆ Conversion ที่เหมาะสมต้องมีเพียง สิทธิ์เข้าถึง API สาธารณะไปยังออบเจ็กต์เดิม ซึ่งแสดงให้เห็นว่านักพัฒนาแอปสามารถเขียน Conversion ที่คล้ายกันกับประเภทที่ตนต้องการได้เช่นกัน

ส่งข้อยกเว้นที่เฉพาะเจาะจงที่เหมาะสม

เมธอดต้องไม่ส่งข้อยกเว้นทั่วไป เช่น java.lang.Exception หรือ java.lang.Throwable แต่ต้องใช้ข้อยกเว้นที่เฉพาะเจาะจงที่เหมาะสม เช่น java.lang.NullPointerException เพื่อให้นักพัฒนาแอปจัดการข้อยกเว้นได้ โดยไม่กว้างเกินไป

ข้อผิดพลาดที่ไม่เกี่ยวข้องกับอาร์กิวเมนต์ที่ระบุโดยตรงในเมธอดที่เรียกใช้แบบสาธารณะควรแสดง java.lang.IllegalStateException แทนที่จะเป็น java.lang.IllegalArgumentException หรือ java.lang.NullPointerException

Listener และ Callback

ต่อไปนี้คือกฎเกี่ยวกับคลาสและเมธอดที่ใช้สำหรับกลไก Listener และ Callback

ชื่อคลาสของ Callback ควรเป็นเอกพจน์

โปรดใช้ MyObjectCallback แทน MyObjectCallbacks

ชื่อเมธอด Callback ควรมีรูปแบบ on

onFooEvent แสดงว่า FooEvent กำลังเกิดขึ้นและควรกระทำการตอบสนอง

กาลในอดีตและปัจจุบันควรอธิบายลักษณะการทำงานของเวลา

ควรตั้งชื่อเมธอด Callback ที่เกี่ยวข้องกับเหตุการณ์เพื่อระบุว่าเหตุการณ์ เกิดขึ้นแล้วหรือกำลังเกิดขึ้น

เช่น หากมีการเรียกใช้เมธอดหลังจากดำเนินการคลิก

public void onClicked()

อย่างไรก็ตาม หากเมธอดมีหน้าที่รับผิดชอบในการดำเนินการคลิก

public boolean onClick()

การลงทะเบียนการติดต่อกลับ

เมื่อเพิ่มหรือนำ Listener หรือ Callback ออกจากออบเจ็กต์ได้ วิธีการที่เกี่ยวข้องควรมีชื่อว่า add และ remove หรือ register และ unregister ใช้รูปแบบการตั้งชื่อที่สอดคล้องกับรูปแบบที่มีอยู่ซึ่งชั้นเรียนหรือชั้นเรียนอื่นๆ ในแพ็กเกจเดียวกันใช้ หากไม่มีตัวอย่างดังกล่าว ให้ใช้เพิ่มและนำออก

เมธอดที่เกี่ยวข้องกับการลงทะเบียนหรือยกเลิกการลงทะเบียนการเรียกกลับควรระบุ ชื่อทั้งหมดของประเภทการเรียกกลับ

public void addFooCallback(@NonNull FooCallback callback);
public void removeFooCallback(@NonNull FooCallback callback);
public void registerFooCallback(@NonNull FooCallback callback);
public void unregisterFooCallback(@NonNull FooCallback callback);

หลีกเลี่ยงตัวรับสำหรับแฮนเดิลการเรียกกลับ

อย่าเพิ่มวิธีการ getFooCallback() นี่เป็นทางออกที่น่าสนใจสำหรับกรณีที่นักพัฒนาแอปอาจต้องการเชื่อมโยงการเรียกกลับที่มีอยู่กับการเรียกกลับของตนเอง แต่ก็เป็นวิธีที่ไม่เสถียรและทำให้นักพัฒนาคอมโพเนนต์พิจารณาสถานะปัจจุบันได้ยาก ตัวอย่างเช่น

  • นักพัฒนาแอป ก. โทรหา setFooCallback(a)
  • นักพัฒนาแอป ข โทรหา setFooCallback(new B(getFooCallback()))
  • นักพัฒนาแอป ก. ต้องการนำการเรียกกลับ a ออก แต่ไม่มีวิธีดำเนินการ หากไม่ทราบประเภทของ B และ B ไม่ได้สร้างขึ้นเพื่ออนุญาตให้ แก้ไขการเรียกกลับที่ห่อหุ้มไว้

ยอมรับ Executor เพื่อควบคุมการเรียกกลับ

เมื่อลงทะเบียนการเรียกกลับที่ไม่มีการคาดการณ์การทำงานแบบหลายเธรดอย่างชัดเจน (แทบทุกที่นอกชุดเครื่องมือ UI) เราขอแนะนำอย่างยิ่งให้ใส่พารามิเตอร์ Executor เป็นส่วนหนึ่งของการลงทะเบียนเพื่อให้ผู้พัฒนาสามารถระบุเธรดที่จะเรียกใช้การเรียกกลับได้

public void registerFooCallback(
    @NonNull @CallbackExecutor Executor executor,
    @NonNull FooCallback callback)

เป็นข้อยกเว้นจากหลักเกณฑ์เกี่ยวกับพารามิเตอร์ที่ไม่บังคับตามปกติ เราอนุญาตให้ระบุโอเวอร์โหลดที่ละเว้น Executor ได้แม้ว่าจะไม่ใช่อาร์กิวเมนต์สุดท้ายในรายการพารามิเตอร์ หากไม่ได้ระบุ Executor ไว้ ระบบควรเรียกใช้การเรียกกลับ ในเทรดหลักโดยใช้ Looper.getMainLooper() และควรบันทึกข้อมูลนี้ ไว้ในเมธอดที่โอเวอร์โหลดที่เกี่ยวข้อง

/**
 * ...
 * 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ข้อควรระวังในการติดตั้งใช้งาน: โปรดทราบว่ารายการต่อไปนี้เป็น ตัวดำเนินการที่ถูกต้อง

public class SynchronousExecutor implements Executor {
    @Override
    public void execute(Runnable r) {
        r.run();
    }
}

ซึ่งหมายความว่าเมื่อใช้ API ที่มีรูปแบบนี้ การใช้งานออบเจ็กต์ Binder ขาเข้าในฝั่งกระบวนการของแอปต้องเรียกใช้Binder.clearCallingIdentity()ก่อนที่จะเรียกใช้แฮนเดิลการเรียกกลับของแอปในExecutorที่แอปจัดหาให้ ด้วยวิธีนี้ โค้ดแอปใดก็ตามที่ใช้ข้อมูลระบุตัวตนของ Binder (เช่น Binder.getCallingUid()) สำหรับการตรวจสอบสิทธิ์จะระบุแหล่งที่มาของโค้ดที่ทำงานอย่างถูกต้องไปยังแอป ไม่ใช่กระบวนการของระบบที่เรียกใช้ในแอป หากผู้ใช้ API ของคุณต้องการข้อมูล UID หรือ PID ของผู้เรียกใช้ ข้อมูลนี้ควรเป็นส่วนที่ชัดเจนของพื้นผิว API ของคุณ แทนที่จะเป็นส่วนที่ซ่อนอยู่ตามตำแหน่งที่ Executor ที่ผู้ใช้ระบุทำงาน

API ของคุณควรรองรับการระบุ Executor ในกรณีที่ประสิทธิภาพมีความสำคัญอย่างยิ่ง แอปอาจต้องเรียกใช้โค้ดทันทีหรือพร้อมกันกับความคิดเห็นจาก API การยอมรับExecutorจะอนุญาตให้ดำเนินการนี้ได้ การสร้าง HandlerThread เพิ่มเติมหรือคล้ายกับแทรมโพลีนจาก จะทำให้กรณีการใช้งานที่ต้องการนี้ไม่สำเร็จ

หากแอปจะเรียกใช้โค้ดที่มีค่าใช้จ่ายสูงในกระบวนการของตนเอง ก็ปล่อยให้แอปทำ วิธีแก้ปัญหาที่นักพัฒนาแอปจะใช้เพื่อหลีกเลี่ยงข้อจำกัดของคุณ จะสนับสนุนได้ยากกว่ามากในระยะยาว

ข้อยกเว้นสำหรับการเรียกกลับครั้งเดียว: เมื่อลักษณะของเหตุการณ์ที่รายงาน กำหนดให้รองรับอินสแตนซ์การเรียกกลับเพียงรายการเดียว ให้ใช้รูปแบบต่อไปนี้

public void setFooCallback(
    @NonNull @CallbackExecutor Executor executor,
    @NonNull FooCallback callback)

public void clearFooCallback()

ใช้ Executor แทน Handler

ในอดีต เราใช้ Handler ของ Android เป็นมาตรฐานสำหรับการเปลี่ยนเส้นทางการดำเนินการเรียกกลับไปยังเธรด Looper ที่เฉพาะเจาะจง เราได้เปลี่ยนมาตรฐานนี้เพื่อใช้ Executor เนื่องจากนักพัฒนาแอปส่วนใหญ่จัดการกลุ่มเธรดของตนเอง ซึ่งทำให้เธรดหลักหรือ UI เป็นเธรด Looper เดียวที่แอปใช้ได้ ใช้ Executor เพื่อให้นักพัฒนาแอปควบคุมสิ่งที่จำเป็นในการนำบริบทการดำเนินการที่มีอยู่/ที่ต้องการกลับมาใช้ซ้ำ

ไลบรารีการทำงานพร้อมกันที่ทันสมัย เช่น kotlinx.coroutines หรือ RxJava มีกลไกการจัดกำหนดการของตัวเองซึ่งจะทำการเรียกใช้ของตัวเองเมื่อจำเป็น ดังนั้นจึงควรให้ความสามารถในการใช้ตัวเรียกใช้โดยตรง (เช่น Runnable::run) เพื่อหลีกเลี่ยงเวลาในการตอบสนองที่เกิดจากการเปลี่ยนเธรด 2 ครั้ง เช่น การส่งต่อ 1 ครั้ง เพื่อโพสต์ในเธรด Looper โดยใช้ Handler ตามด้วยการส่งต่ออีกครั้งจาก เฟรมเวิร์กการทำงานพร้อมกันของแอป

โดยปกติแล้ว เราจะไม่ยกเว้นหลักเกณฑ์นี้ การอุทธรณ์ที่พบบ่อยสำหรับการยกเว้น ได้แก่

ฉันต้องใช้ Looper เพราะต้องมี Looper เพื่อepoll สำหรับกิจกรรม เราอนุมัติคำขอยกเว้นนี้เนื่องจากไม่สามารถรับสิทธิประโยชน์ของ Executor ได้ในสถานการณ์นี้

ฉันไม่ต้องการให้โค้ดแอปบล็อกการเผยแพร่เหตุการณ์ในเธรด โดยปกติแล้ว คำขอข้อยกเว้นนี้จะไม่ได้รับอนุมัติสำหรับโค้ดที่ทำงานในกระบวนการของแอป แอปที่ทำผิดพลาดในเรื่องนี้จะส่งผลเสียต่อตัวแอปเองเท่านั้น ไม่ส่งผลต่อ สุขภาพโดยรวมของระบบ แอปที่ทำได้อย่างถูกต้องหรือใช้เฟรมเวิร์กการทำงานพร้อมกันทั่วไปไม่ควรต้องเสียค่าปรับด้านเวลาในการตอบสนองเพิ่มเติม

Handler สอดคล้องกับ API อื่นๆ ที่คล้ายกันในคลาสเดียวกันในระดับท้องถิ่น คำขอข้อยกเว้นนี้จะได้รับอนุมัติตามสถานการณ์ เราต้องการให้เพิ่มการโอเวอร์โหลดที่อิงตาม Executor และย้ายข้อมูลการใช้งาน Handler ไปใช้การใช้งาน Executor ใหม่ (myHandler::post เป็นExecutorที่ถูกต้อง) ระบบอาจอนุญาตให้เพิ่มเมธอดใหม่ที่อิงตาม Handler ทั้งนี้ขึ้นอยู่กับขนาดของคลาส จำนวนเมธอด Handler ที่มีอยู่ และความเป็นไปได้ที่นักพัฒนาซอฟต์แวร์จะต้องใช้เมธอดที่มีอยู่ซึ่งอิงตาม Handler ควบคู่ไปกับเมธอดใหม่

ความสมมาตรในการจดทะเบียน

หากมีวิธีเพิ่มหรือลงทะเบียนสิ่งใด ก็ควรมีวิธีนำออก/ยกเลิกการลงทะเบียนด้วย วิธีการ

registerThing(Thing)

ควรมี

unregisterThing(Thing)

ระบุตัวระบุคำขอ

หากนักพัฒนาแอปควรใช้ฟังก์ชันเรียกกลับซ้ำ ให้ระบุออบเจ็กต์ตัวระบุ เพื่อเชื่อมโยงฟังก์ชันเรียกกลับกับคำขอ

class RequestParameters {
  public int getId() { ... }
}

class RequestExecutor {
  public void executeRequest(
    RequestParameters parameters,
    Consumer<RequestParameters> onRequestCompletedListener) { ... }
}

ออบเจ็กต์การเรียกกลับแบบหลายเมธอด

การเรียกกลับแบบหลายวิธีควรใช้ interface และใช้วิธี default เมื่อเพิ่มลงในอินเทอร์เฟซที่เผยแพร่ก่อนหน้านี้ ก่อนหน้านี้ หลักเกณฑ์นี้ แนะนำให้ใช้ abstract class เนื่องจากไม่มีเมธอด default ใน Java 7

public interface MostlyOptionalCallback {
  void onImportantAction();
  default void onOptionalInformation() {
    // Empty stub, this method is optional.
  }
}

ใช้ android.os.OutcomeReceiver เมื่อสร้างโมเดลการเรียกฟังก์ชันที่ไม่บล็อก

OutcomeReceiver<R,E> รายงานค่าผลลัพธ์ R เมื่อสำเร็จ หรือ E : Throwable ในกรณีอื่นๆ ซึ่งเป็นสิ่งเดียวกับการเรียกเมธอดธรรมดา ใช้ OutcomeReceiver เป็นประเภทการเรียกกลับ เมื่อแปลงเมธอดที่บล็อกซึ่งส่งคืนผลลัพธ์หรือส่งข้อยกเว้น ไปยังเมธอดแบบอะซิงโครนัสที่ไม่บล็อก

interface FooType {
  // Before:
  public FooResult requestFoo(FooRequest request);

  // After:
  public void requestFooAsync(FooRequest request, Executor executor,
      OutcomeReceiver<FooResult, Throwable> callback);
}

เมธอดแบบไม่พร้อมกันที่แปลงด้วยวิธีนี้จะส่งคืน void เสมอ ผลลัพธ์ใดๆ ที่requestFooจะแสดงจะได้รับการรายงานไปยังrequestFooAsynccallback พารามิเตอร์OutcomeReceiver.onResultแทนโดยการเรียกใช้ใน executor ที่ระบุ ระบบจะรายงานข้อยกเว้นที่ requestFoo จะส่งไปยังเมธอด OutcomeReceiver.onError ในลักษณะเดียวกันแทน

การใช้ OutcomeReceiver สำหรับการรายงานผลลัพธ์ของเมธอดแบบอะซิงโครนัสยังช่วยให้มี Wrapper ของ Kotlin suspend fun สำหรับเมธอดแบบอะซิงโครนัสโดยใช้ส่วนขยาย Continuation.asOutcomeReceiver จาก androidx.core:core-ktx ด้วย

suspend fun FooType.requestFoo(request: FooRequest): FooResult =
  suspendCancellableCoroutine { continuation ->
    requestFooAsync(request, Runnable::run, continuation.asOutcomeReceiver())
  }

ส่วนขยายเช่นนี้ช่วยให้ไคลเอ็นต์ Kotlin เรียกใช้เมธอดแบบอะซิงโครนัสที่ไม่บล็อกได้ อย่างสะดวกสบายด้วยการเรียกใช้ฟังก์ชันธรรมดาโดยไม่ต้องบล็อกเธรดที่เรียกใช้ ส่วนขยายแบบ 1:1 สำหรับ API ของแพลตฟอร์มเหล่านี้อาจมีให้ใช้งานเป็นส่วนหนึ่งของandroidx.core:core-ktxอาร์ติแฟกต์ใน Jetpack เมื่อใช้ร่วมกับการตรวจสอบความเข้ากันได้และการพิจารณาเวอร์ชันมาตรฐาน ดูข้อมูลเพิ่มเติม ข้อควรพิจารณาในการยกเลิก และตัวอย่างได้ในเอกสารประกอบสำหรับ asOutcomeReceiver

เมธอดแบบอะซิงโครนัสที่ไม่ตรงกับความหมายของเมธอดที่ส่งคืนผลลัพธ์หรือ ส่งข้อยกเว้นเมื่อการทำงานเสร็จสมบูรณ์ไม่ควรใช้ OutcomeReceiver เป็นประเภทการเรียกกลับ แต่ให้พิจารณาใช้ตัวเลือกอื่นๆ ที่ระบุไว้ในส่วนต่อไปนี้แทน

เลือกใช้อินเทอร์เฟซที่ใช้งานได้แทนการสร้างประเภท Single Abstract Method (SAM) ใหม่

API ระดับ 24 ได้เพิ่มjava.util.function.* (เอกสารอ้างอิง) ประเภท ซึ่งมีอินเทอร์เฟซ SAM ทั่วไป เช่น Consumer<T> ที่ เหมาะสำหรับใช้เป็น Lambda ของการเรียกกลับ ในหลายกรณี การสร้างอินเทอร์เฟซ SAM ใหม่ ให้คุณค่าเพียงเล็กน้อยในแง่ของความปลอดภัยของประเภทหรือการสื่อสารความตั้งใจ ขณะที่ ขยายพื้นที่ผิวของ Android API โดยไม่จำเป็น

ลองใช้ส่วนติดต่อทั่วไปเหล่านี้แทนการสร้างส่วนติดต่อใหม่

ตำแหน่งของพารามิเตอร์ SAM

ควรวางพารามิเตอร์ SAM ไว้สุดท้ายเพื่อให้ใช้จาก Kotlin ได้อย่างเป็นธรรมชาติ แม้ว่าเมธอดจะมีการโอเวอร์โหลดด้วยพารามิเตอร์เพิ่มเติมก็ตาม

public void schedule(Runnable runnable)

public void schedule(int delay, Runnable runnable)

เอกสาร

ต่อไปนี้คือกฎเกี่ยวกับเอกสารสาธารณะ (Javadoc) สำหรับ API

ต้องจัดทำเอกสาร API สาธารณะทั้งหมด

API สาธารณะทั้งหมดต้องมีเอกสารประกอบที่เพียงพอเพื่ออธิบายวิธีที่นักพัฒนาซอฟต์แวร์ จะใช้ API สมมติว่านักพัฒนาซอฟต์แวร์พบเมธอดโดยใช้การเติมข้อความอัตโนมัติหรือขณะเรียกดูเอกสารอ้างอิง API และมีบริบทเพียงเล็กน้อยจากพื้นผิว API ที่อยู่ติดกัน (เช่น คลาสเดียวกัน)

วิธีการ

ต้องบันทึกพารามิเตอร์ของเมธอดและค่าที่ส่งคืนโดยใช้คำอธิบายประกอบ @param และ @return ตามลำดับ จัดรูปแบบเนื้อหา Javadoc ราวกับว่ามีข้อความ "เมธอดนี้..." นำหน้า

ในกรณีที่เมธอดไม่มีพารามิเตอร์ ไม่มีข้อควรพิจารณาพิเศษ และ แสดงผลตามที่ชื่อเมธอดระบุ คุณสามารถละเว้น @return และ เขียนเอกสารในลักษณะที่คล้ายกับตัวอย่างต่อไปนี้

/**
 * Returns the priority of the thread.
 */
@IntRange(from = 1, to = 10)
public int getPriority() { ... }

เอกสารควรลิงก์ไปยังเอกสารอื่นๆ สำหรับค่าคงที่ เมธอด และองค์ประกอบอื่นๆ ที่เกี่ยวข้อง ใช้แท็ก Javadoc (เช่น @see และ {@link foo}) ไม่ใช่แค่ คำที่เป็นข้อความธรรมดา

สำหรับตัวอย่างแหล่งข้อมูลต่อไปนี้

public static final int FOO = 0;
public static final int BAR = 1;

อย่าใช้ข้อความธรรมดาหรือแบบอักษรโค้ด

/**
 * 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) { ... }

แต่ให้ใช้ลิงก์แทน

/**
 * Sets value to one of {@link #FOO} or {@link #BAR}.
 *
 * @param value the value being set
 */
public void setValue(@ValueType int value) { ... }

โปรดทราบว่าการใช้IntDefคำอธิบายประกอบ เช่น @ValueType ในพารามิเตอร์ จะสร้างเอกสารประกอบโดยอัตโนมัติซึ่งระบุประเภทที่อนุญาต ดูข้อมูลเพิ่มเติมเกี่ยวกับ IntDef ได้ที่คำแนะนำเกี่ยวกับคำอธิบายประกอบ

เรียกใช้ update-api หรือ docs target เมื่อเพิ่ม Javadoc

กฎนี้มีความสำคัญอย่างยิ่งเมื่อเพิ่มแท็ก @link หรือ @see และตรวจสอบว่าเอาต์พุตมีลักษณะตามที่คาดไว้ เอาต์พุต ERROR ใน Javadoc มักเกิดจากลิงก์ที่ไม่ถูกต้อง ทั้งเป้าหมาย update-api หรือ docs Make จะทำการตรวจสอบนี้ แต่docs อาจเร็วกว่าหากคุณเปลี่ยนเฉพาะ Javadoc และไม่จำเป็นต้องเรียกใช้เป้าหมาย update-api

ใช้ {@code foo} เพื่อแยกความแตกต่างของค่า Java

ห่อหุ้มค่า Java เช่น true, false และ null ด้วย {@code...} เพื่อ แยกความแตกต่างจากข้อความในเอกสาร

เมื่อเขียนเอกสารในแหล่งที่มาของ Kotlin คุณสามารถใส่โค้ดไว้ในเครื่องหมายแบ็กทิก เช่นเดียวกับมาร์กดาวน์

ข้อมูลสรุป @param และ @return ควรเป็นส่วนของประโยคเดียว

ข้อมูลสรุปพารามิเตอร์และค่าที่ส่งคืนควรขึ้นต้นด้วยอักขระตัวพิมพ์เล็กและ มีเพียงส่วนของประโยคเดียว หากมีข้อมูลเพิ่มเติมที่ ยาวเกิน 1 ประโยค ให้ย้ายข้อมูลนั้นไปไว้ในเนื้อหา Javadoc ของเมธอด

/**
 * @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.
 */

ควรเปลี่ยนเป็น

/**
 * @param e element to be appended to this list, must be non-{@code null}
 * @return {@code true} on success, {@code false} otherwise
 */

คำอธิบายประกอบในเอกสารต้องมีคำอธิบาย

อธิบายเหตุผลที่ซ่อนคำอธิบายประกอบ @hide และ @removed จาก API สาธารณะ ระบุวิธีการแทนที่องค์ประกอบ API ที่ทำเครื่องหมายด้วยคำอธิบายประกอบ @deprecated

ใช้ @throws เพื่อบันทึกข้อยกเว้น

หากเมธอดส่งกลับข้อยกเว้นที่ตรวจสอบแล้ว เช่น IOException ให้บันทึกข้อยกเว้นด้วย @throws สำหรับ API ที่มาจาก Kotlin ซึ่งมีไว้สำหรับใช้โดยไคลเอ็นต์ Java ให้ใส่คำอธิบายประกอบฟังก์ชันด้วย @Throws

หากเมธอดส่งข้อยกเว้นที่ไม่ได้ตรวจสอบซึ่งบ่งบอกถึงข้อผิดพลาดที่ป้องกันได้ เช่น IllegalArgumentException หรือ IllegalStateException ให้บันทึกข้อยกเว้นพร้อมคำอธิบายว่าเหตุใดจึงส่งข้อยกเว้น ข้อยกเว้นที่ส่งคืนควรระบุเหตุผลที่ส่งคืนด้วย

กรณีที่ไม่ได้ตรวจสอบข้อยกเว้นบางอย่างถือว่าเป็นการยกเว้นโดยนัยและไม่จำเป็นต้อง บันทึก เช่น NullPointerException หรือ IllegalArgumentException ในกรณีที่อาร์กิวเมนต์ไม่ตรงกับ @IntDef หรือคำอธิบายประกอบที่คล้ายกันซึ่งฝัง สัญญา API ไว้ในลายเซ็นของเมธอด

/**
 * ...
 * @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.");
  }
  // ...

หรือใน 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")
    }
  }
  // ...

หากเมธอดเรียกใช้โค้ดแบบอะซิงโครนัสที่อาจทำให้เกิดข้อยกเว้น ให้พิจารณา วิธีที่นักพัฒนาแอปค้นหาและตอบสนองต่อข้อยกเว้นดังกล่าว โดยปกติ ขั้นตอนนี้จะเกี่ยวข้องกับการส่งต่อข้อยกเว้นไปยังการเรียกกลับและการบันทึก ข้อยกเว้นที่เกิดขึ้นในเมธอดที่รับข้อยกเว้นเหล่านั้น ไม่ควรบันทึกข้อยกเว้นแบบไม่พร้อมกันด้วย @throws เว้นแต่จะมีการส่งต่อข้อยกเว้นจาก เมธอดที่มีคำอธิบายประกอบ

จบประโยคแรกของเอกสารด้วยจุด

เครื่องมือ Doclava จะแยกวิเคราะห์เอกสารอย่างง่ายๆ และสิ้นสุดเอกสารสรุป (ประโยคแรกที่ใช้ในคำอธิบายด่วนที่ด้านบนของเอกสารคลาส) ทันทีที่เห็นจุด (.) ตามด้วยช่องว่าง ซึ่งทำให้เกิดปัญหา 2 อย่างดังนี้

  • หากเอกสารย่อไม่ได้ลงท้ายด้วยจุด และหากสมาชิกรายนั้นได้รับเอกสารที่เครื่องมือเลือกมาเป็นมรดก สรุปก็จะเลือกเอกสารที่ได้รับเป็นมรดกเหล่านั้นด้วย เช่น ดู actionBarTabStyle ในเอกสาร R.attr ซึ่งมีคำอธิบายของมิติข้อมูลที่เพิ่มลงในเรื่องย่อ
  • หลีกเลี่ยง "เช่น" ในประโยคแรกด้วยเหตุผลเดียวกัน เนื่องจาก Doclava จะสิ้นสุด เอกสารข้อมูลสรุปหลังจาก "เช่น" เช่น ดู TEXT_ALIGNMENT_CENTER ใน View.java โปรดทราบว่า Metalava จะแก้ไขข้อผิดพลาดนี้โดยอัตโนมัติด้วยการแทรก ช่องว่างที่ไม่ตัดคำหลังจุด แต่คุณไม่ควรทำผิดพลาดนี้ตั้งแต่ แรก

จัดรูปแบบเอกสารให้แสดงผลใน HTML

Javadoc จะแสดงใน HTML ดังนั้นให้จัดรูปแบบเอกสารเหล่านี้ตามนั้น

  • การขึ้นบรรทัดใหม่ควรใช้แท็ก <p> ที่ชัดเจน อย่าเพิ่มแท็กปิด </p>

  • อย่าใช้ ASCII เพื่อแสดงรายการหรือตาราง

  • รายการควรใช้ <ul> หรือ <ol> สำหรับรายการที่ไม่เรียงลำดับและเรียงลำดับตามลำดับ แต่ละรายการควรขึ้นต้นด้วยแท็ก <li> แต่ไม่จำเป็นต้องมีแท็กปิด </li> ต้องมีแท็กปิด </ul> หรือ </ol> หลังรายการสุดท้าย

  • ตารางควรใช้ <table>, <tr> สำหรับแถว <th> สำหรับส่วนหัว และ <td> สำหรับเซลล์ แท็กตารางทั้งหมดต้องมีแท็กปิดที่ตรงกัน คุณใช้ class="deprecated" ในแท็กใดก็ได้เพื่อระบุการเลิกใช้งาน

  • หากต้องการสร้างแบบอักษรโค้ดแบบแทรกในบรรทัด ให้ใช้ {@code foo}

  • หากต้องการสร้างบล็อกโค้ด ให้ใช้ <pre>

  • ข้อความทั้งหมดภายในบล็อก <pre> จะได้รับการแยกวิเคราะห์โดยเบราว์เซอร์ ดังนั้นโปรดระมัดระวังเกี่ยวกับ วงเล็บ <> คุณหลีกเลี่ยงได้โดยใช้เอนทิตี HTML &lt; และ &gt;

  • หรือจะปล่อยให้มีวงเล็บธรรมดา <> ในข้อมูลโค้ดก็ได้ หากคุณ ครอบส่วนที่ละเมิดด้วย {@code foo} เช่น

    <pre>{@code <manifest>}</pre>
    

ทําตามคู่มือรูปแบบเอกสารอ้างอิง API

เพื่อให้รูปแบบของข้อมูลสรุปของคลาส คำอธิบายเมธอด คำอธิบายพารามิเตอร์ และรายการอื่นๆ สอดคล้องกัน ให้ทำตามคำแนะนำใน หลักเกณฑ์ภาษา Java อย่างเป็นทางการที่ วิธีเขียนความคิดเห็นในเอกสารสำหรับเครื่องมือ Javadoc

กฎเฉพาะของ Android Framework

กฎเหล่านี้เกี่ยวข้องกับ API, รูปแบบ และโครงสร้างข้อมูลที่เฉพาะเจาะจงสำหรับ API และลักษณะการทำงานที่สร้างไว้ในเฟรมเวิร์ก Android (เช่น Bundle หรือ Parcelable)

เครื่องมือสร้างความตั้งใจควรใช้รูปแบบ create*Intent()

ครีเอเตอร์สำหรับเจตนาควรใช้วิธีการที่ชื่อว่า createFooIntent()

ใช้ Bundle แทนการสร้างโครงสร้างข้อมูลอเนกประสงค์ใหม่

หลีกเลี่ยงการสร้างโครงสร้างข้อมูลอเนกประสงค์ใหม่เพื่อแสดงการแมปคีย์ที่กำหนดเองกับค่าที่พิมพ์ ให้ลองใช้ Bundle แทน

โดยปกติปัญหานี้จะเกิดขึ้นเมื่อเขียน API ของแพลตฟอร์มที่ทำหน้าที่เป็นช่องทางการสื่อสารระหว่างแอปและบริการที่ไม่ใช่แพลตฟอร์ม ซึ่งแพลตฟอร์มจะไม่อ่านข้อมูลที่ส่งผ่านช่องทาง และอาจมีการกำหนดสัญญา API บางส่วนภายนอกแพลตฟอร์ม (เช่น ในไลบรารี Jetpack)

ในกรณีที่แพลตฟอร์มอ่านข้อมูล ให้หลีกเลี่ยงการใช้ Bundle และ เลือกใช้คลาสข้อมูลที่มีการพิมพ์อย่างเข้มงวด

การใช้งาน Parcelable ต้องมีฟิลด์ CREATOR แบบสาธารณะ

การขยาย Parcelable จะแสดงผ่าน CREATOR ไม่ใช่เครื่องมือสร้างดิบ หากคลาสใช้ Parcelable ฟิลด์ CREATOR ของคลาสนั้นต้องเป็น API สาธารณะด้วย และตัวสร้างคลาสที่รับอาร์กิวเมนต์ Parcel ต้องเป็นแบบส่วนตัว

ใช้ CharSequence สำหรับสตริง UI

เมื่อแสดงสตริงในอินเทอร์เฟซผู้ใช้ ให้ใช้ CharSequence เพื่ออนุญาตอินสแตนซ์ Spannable

หากเป็นเพียงคีย์หรือป้ายกำกับหรือค่าอื่นๆ ที่ผู้ใช้มองไม่เห็น Stringก็ใช้ได้

หลีกเลี่ยงการใช้ Enum

IntDef ต้องใช้กับ enum ใน API ของแพลตฟอร์มทั้งหมด และควรพิจารณาอย่างยิ่ง ใน API ของไลบรารีที่ไม่ได้รวมไว้ ใช้ Enum เฉพาะเมื่อคุณมั่นใจว่าจะไม่มีการเพิ่มค่าใหม่

ประโยชน์ของIntDef

  • ช่วยให้เพิ่มมูลค่าได้เมื่อเวลาผ่านไป
    • คำสั่ง Kotlin when อาจล้มเหลวขณะรันไทม์หากคำสั่งเหล่านั้น ไม่ครอบคลุมอีกต่อไปเนื่องจากมีการเพิ่มค่า Enum ในแพลตฟอร์ม
  • ไม่มีคลาสหรือออบเจ็กต์ที่ใช้ในรันไทม์ มีเพียงไพรมิทีฟ
    • แม้ว่า R8 หรือการลดขนาดจะช่วยหลีกเลี่ยงค่าใช้จ่ายนี้สำหรับ API ของไลบรารีที่ไม่ได้รวมกลุ่มได้ แต่การเพิ่มประสิทธิภาพนี้จะไม่มีผลต่อคลาส API ของแพลตฟอร์ม

ประโยชน์ของ Enum

  • ฟีเจอร์ภาษาที่เป็นสำนวนของ Java, Kotlin
  • เปิดใช้การสลับที่ครอบคลุม when การใช้คำสั่ง
    • หมายเหตุ - ค่าต้องไม่เปลี่ยนแปลงเมื่อเวลาผ่านไป โปรดดูรายการก่อนหน้า
  • การตั้งชื่อที่ชัดเจนและค้นพบได้
  • เปิดใช้การยืนยันขณะคอมไพล์
    • เช่น when ใน Kotlin ที่แสดงผลค่า
  • เป็นคลาสที่ใช้งานได้ซึ่งสามารถใช้การเชื่อมต่อ มีตัวช่วยแบบคงที่ แสดงเมธอดของสมาชิกหรือส่วนขยาย และแสดงฟิลด์

ทำตามลำดับชั้นเลเยอร์ของแพ็กเกจ Android

ลำดับชั้นของแพ็กเกจ android.* มีการจัดลำดับโดยนัย ซึ่งแพ็กเกจระดับล่าง จะขึ้นอยู่กับแพ็กเกจระดับสูงกว่าไม่ได้

หลีกเลี่ยงการอ้างอิงถึง Google, บริษัทอื่นๆ และผลิตภัณฑ์ของบริษัทเหล่านั้น

แพลตฟอร์ม Android เป็นโปรเจ็กต์โอเพนซอร์สและมีเป้าหมายที่จะไม่ขึ้นอยู่กับผู้ให้บริการ API ควรเป็นแบบทั่วไปและผู้ติดตั้งระบบหรือแอปที่มีสิทธิ์ที่จำเป็นสามารถใช้งานได้เท่าเทียมกัน

การติดตั้งใช้งาน Parcelable ควรเป็นขั้นสุดท้าย

ระบบจะโหลดคลาส Parcelable ที่แพลตฟอร์มกำหนดจาก framework.jar เสมอ ดังนั้นแอปจึงไม่สามารถแทนที่การใช้งาน Parcelable ได้

หากแอปที่ส่งขยาย Parcelable แอปที่รับจะไม่มีการติดตั้งใช้งานที่กำหนดเองของผู้ส่งเพื่อแกะ หมายเหตุเกี่ยวกับความเข้ากันได้แบบย้อนหลัง: หากคลาสของคุณไม่เป็นคลาสสุดท้ายในอดีต แต่ไม่มีตัวสร้างที่พร้อมใช้งานแบบสาธารณะ คุณยังคงทำเครื่องหมายเป็น final ได้

เมธอดที่เรียกใช้กระบวนการของระบบควรส่งต่อ RemoteException เป็น RuntimeException

RemoteException มักจะเกิดจาก AIDL ภายใน และบ่งชี้ว่ากระบวนการของระบบหยุดทำงาน หรือแอปพยายามส่งข้อมูลมากเกินไป ในทั้ง 2 กรณี API สาธารณะควรส่งต่อเป็น RuntimeException เพื่อป้องกันไม่ให้แอปคงการตัดสินใจด้านความปลอดภัยหรือนโยบาย

หากคุณทราบว่าอีกฝั่งของBinderการเรียกใช้เป็นกระบวนการของระบบ โค้ดมาตรฐานนี้คือแนวทางปฏิบัติแนะนำ

try {
    ...
} catch (RemoteException e) {
    throw e.rethrowFromSystemServer();
}

ส่งข้อยกเว้นที่เฉพาะเจาะจงสำหรับการเปลี่ยนแปลง API

ลักษณะการทำงานของ API สาธารณะอาจเปลี่ยนแปลงในระดับ API ต่างๆ และทำให้แอปขัดข้อง (เช่น เพื่อบังคับใช้นโยบายความปลอดภัยใหม่)

เมื่อ API ต้องส่งข้อยกเว้นสำหรับคำขอที่ถูกต้องก่อนหน้านี้ ให้ส่งข้อยกเว้นใหม่ที่เฉพาะเจาะจงแทนข้อยกเว้นทั่วไป เช่น ExportedFlagRequired แทนที่จะเป็น SecurityException (และ ExportedFlagRequired สามารถขยาย SecurityException ได้)

ซึ่งจะช่วยให้นักพัฒนาแอปและเครื่องมือตรวจจับการเปลี่ยนแปลงลักษณะการทำงานของ API ได้

ใช้ตัวสร้างสำเนาแทนการโคลน

เราไม่แนะนำให้ใช้วิธี clone() ของ Java เนื่องจากไม่มีสัญญา API ที่คลาส Object จัดให้ และความยากในการขยายคลาสที่ใช้ clone() แต่ให้ใช้ตัวสร้างสำเนาที่รับออบเจ็กต์ ประเภทเดียวกันแทน

/**
 * Constructs a shallow copy of {@code other}.
 */
public Foo(Foo other)

คลาสที่ใช้ Builder ในการสร้างควรพิจารณาเพิ่มตัวสร้างสำเนา Builder เพื่ออนุญาตให้แก้ไขสำเนาได้

public class Foo {
    public static final class Builder {
        /**
         * Constructs a Foo builder using data from {@code other}.
         */
        public Builder(Foo other)

ใช้ ParcelFileDescriptor แทน FileDescriptor

ออบเจ็กต์ java.io.FileDescriptor มีคำจำกัดความการเป็นเจ้าของที่ไม่ดี ซึ่ง อาจส่งผลให้เกิดข้อบกพร่องที่ทำให้ใช้หลังจากปิดไปแล้ว API ควรแสดงหรือยอมรับอินสแตนซ์ ParcelFileDescriptor แทน โค้ดเดิมสามารถแปลงระหว่าง PFD กับ FD ได้หากจำเป็นโดยใช้ dup() หรือ getFileDescriptor()

หลีกเลี่ยงการใช้ค่าตัวเลขที่มีขนาดไม่ปกติ

หลีกเลี่ยงการใช้ค่า short หรือ byte โดยตรง เนื่องจากค่าเหล่านี้มักจะจำกัดวิธีที่คุณ อาจพัฒนา API ในอนาคต

หลีกเลี่ยงการใช้ BitSet

java.util.BitSet เหมาะสำหรับการติดตั้งใช้งาน แต่ไม่เหมาะสำหรับ API สาธารณะ โดยจะ เปลี่ยนแปลงได้ ต้องมีการจัดสรรสำหรับการเรียกเมธอดความถี่สูง และไม่มี ความหมายเชิงความหมายสำหรับสิ่งที่แต่ละบิตแสดง

สำหรับสถานการณ์ที่มีประสิทธิภาพสูง ให้ใช้ int หรือ long กับ @IntDef สําหรับสถานการณ์ที่มีประสิทธิภาพต่ำ ให้พิจารณาSet<EnumType> สำหรับข้อมูลไบนารีดิบ ให้ใช้ byte[]

ใช้ android.net.Uri

android.net.Uri เป็นการห่อหุ้มที่แนะนำสำหรับ URI ใน Android API

หลีกเลี่ยง java.net.URI เนื่องจากมีความเข้มงวดมากเกินไปในการแยกวิเคราะห์ URI และอย่าใช้ java.net.URL เนื่องจากคำจำกัดความของความเท่าเทียมกันนั้นไม่ถูกต้องอย่างมาก

ซ่อนคำอธิบายประกอบที่ทำเครื่องหมายเป็น @IntDef, @LongDef หรือ @StringDef

คำอธิบายประกอบที่ทำเครื่องหมายเป็น @IntDef, @LongDef หรือ @StringDef แสดงถึงชุดค่าคงที่ที่ถูกต้องซึ่งส่งไปยัง API ได้ อย่างไรก็ตาม เมื่อส่งออกเป็น API เอง คอมไพเลอร์จะแทรกค่าคงที่ และเหลือเพียงค่า (ซึ่งตอนนี้ไม่มีประโยชน์แล้ว) ใน Stub API ของคำอธิบายประกอบ (สำหรับแพลตฟอร์ม) หรือ JAR (สำหรับ ไลบรารี)

ดังนั้น การใช้งานคำอธิบายประกอบเหล่านี้ต้องทำเครื่องหมายด้วยคำอธิบายประกอบ @hide docs ในแพลตฟอร์มหรือ@RestrictTo.Scope.LIBRARY)คำอธิบายประกอบโค้ดใน ไลบรารี ต้องทำเครื่องหมาย @Retention(RetentionPolicy.SOURCE) ในทั้ง 2 กรณีเพื่อป้องกันไม่ให้ปรากฏใน Stub API หรือ JAR

@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
  STREAM_TYPE_FULL_IMAGE_DATA,
  STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}

เมื่อสร้าง SDK ของแพลตฟอร์มและ AAR ของไลบรารี เครื่องมือจะดึงคำอธิบายประกอบ และรวมไว้แยกจากแหล่งที่มาที่คอมไพล์แล้ว Android Studio จะอ่านรูปแบบที่รวมไว้และบังคับใช้คำจำกัดความของประเภท

อย่าเพิ่มคีย์ผู้ให้บริการการตั้งค่าใหม่

อย่าเปิดเผยคีย์ใหม่จาก Settings.Global Settings.System หรือ Settings.Secure

แต่ให้เพิ่ม Java API ของ Getter และ Setter ที่เหมาะสมในคลาสที่เกี่ยวข้อง ซึ่งโดยปกติจะเป็นคลาส "Manager" เพิ่มกลไกการฟังหรือการออกอากาศเพื่อแจ้งให้ ไคลเอ็นต์ทราบถึงการเปลี่ยนแปลงตามต้องการ

SettingsProvider มีปัญหาหลายอย่างเมื่อเทียบกับ Getter/Setter ดังนี้

  • ไม่มีความปลอดภัยของประเภท
  • ไม่มีวิธีแบบรวมในการระบุมูลค่าเริ่มต้น
  • ไม่มีวิธีที่เหมาะสมในการปรับแต่งสิทธิ์
    • เช่น คุณไม่สามารถปกป้องการตั้งค่าด้วยสิทธิ์ที่กำหนดเองได้
  • ไม่มีวิธีที่เหมาะสมในการเพิ่มตรรกะที่กำหนดเองอย่างถูกต้อง
    • เช่น คุณไม่สามารถเปลี่ยนค่าของการตั้งค่า A ตามค่าของการตั้งค่า B ได้

ตัวอย่าง Settings.Secure.LOCATION_MODE มีมานานแล้ว แต่ทีมตำแหน่งได้เลิกใช้งานเพื่อ Java API ที่เหมาะสม LocationManager.isLocationEnabled() และ MODE_CHANGED_ACTION การออกอากาศ ซึ่งทำให้ทีมมีความยืดหยุ่นมากขึ้น และตอนนี้ความหมายของ API ก็ชัดเจนขึ้นมาก

อย่าขยาย Activity และ AsyncTask

AsyncTask เป็นรายละเอียดการใช้งาน แต่ให้แสดง Listener หรือใน androidx ให้แสดง ListenableFuture API แทน

Activity คลาสย่อยไม่สามารถสร้างได้ การขยายกิจกรรมสำหรับฟีเจอร์จะทำให้ฟีเจอร์นั้นใช้ร่วมกับฟีเจอร์อื่นๆ ที่กำหนดให้ผู้ใช้ทำสิ่งเดียวกันไม่ได้ แต่ให้ใช้การคอมโพสโดยใช้เครื่องมือต่างๆ เช่น LifecycleObserver แทน

ใช้ getUser() ของบริบท

คลาสที่เชื่อมโยงกับ Context เช่น ข้อมูลที่ส่งคืนจาก Context.getSystemService() ควรใช้ผู้ใช้ที่เชื่อมโยงกับ Context แทน การแสดงสมาชิกที่กำหนดเป้าหมายไปยังผู้ใช้ที่เฉพาะเจาะจง

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);
  }
}

ข้อยกเว้น: เมธอดอาจยอมรับอาร์กิวเมนต์ผู้ใช้หากยอมรับค่าที่ไม่ได้ แสดงถึงผู้ใช้รายเดียว เช่น UserHandle.ALL

ใช้ UserHandle แทน int ธรรมดา

UserHandle ขอแนะนำให้ใช้เพื่อความปลอดภัยของประเภทและหลีกเลี่ยงการรวมรหัสผู้ใช้ กับ uid

Foobar getFoobarForUser(UserHandle user);
Foobar getFoobarForUser(int userId);

หากหลีกเลี่ยงไม่ได้ ให้ใส่คำอธิบายประกอบ int ที่แสดงรหัสผู้ใช้ด้วย @UserIdInt

Foobar getFoobarForUser(@UserIdInt int user);

ใช้ Listener หรือการเรียกกลับแทนการออกอากาศ Intent

Broadcast Intent มีประสิทธิภาพมาก แต่ก็ทำให้เกิดพฤติกรรมที่เกิดขึ้นใหม่ ซึ่งอาจส่งผลเสียต่อสุขภาพของระบบ ดังนั้นจึงควรเพิ่ม Broadcast Intent ใหม่ อย่างรอบคอบ

ข้อกังวลบางประการที่ทำให้เราไม่แนะนำให้เปิดตัว Intent การออกอากาศใหม่มีดังนี้

  • เมื่อส่งการออกอากาศโดยไม่มีแฟล็ก FLAG_RECEIVER_REGISTERED_ONLY ระบบจะบังคับให้เริ่มแอปที่ยังไม่ได้ทำงาน แม้ว่าบางครั้งนี่อาจเป็นผลลัพธ์ที่ต้องการ แต่ก็อาจส่งผลให้แอปหลายสิบแอปทำงานพร้อมกัน ซึ่งส่งผลเสียต่อสุขภาพของระบบ เราขอแนะนำให้ใช้กลยุทธ์อื่น เช่น JobScheduler เพื่อประสานงานได้ดียิ่งขึ้นเมื่อมี ข้อกำหนดเบื้องต้นต่างๆ ครบถ้วน

  • เมื่อส่งการออกอากาศ คุณจะกรองหรือปรับเนื้อหาที่ส่งไปยังแอปได้น้อยมาก ซึ่งทำให้ตอบสนองต่อข้อกังวลด้านความเป็นส่วนตัวในอนาคตได้ยากหรือเป็นไปไม่ได้ หรือทำการเปลี่ยนแปลงลักษณะการทำงานตาม SDK เป้าหมายของแอปที่รับ

  • เนื่องจากคิวการออกอากาศเป็นทรัพยากรที่ใช้ร่วมกัน คิวจึงอาจทำงานหนักเกินไปและ อาจทำให้การนำส่งกิจกรรมไม่ทันเวลา เราพบคิวการออกอากาศหลายรายการ ที่ใช้งานจริงซึ่งมีเวลาในการตอบสนองแบบ End-to-End นาน 10 นาที หรือนานกว่านั้น

ด้วยเหตุนี้ เราจึงแนะนำให้ฟีเจอร์ใหม่ๆ พิจารณาใช้ Listener หรือ Callback หรือสิ่งอำนวยความสะดวกอื่นๆ เช่น JobScheduler แทน Broadcast Intent

ในกรณีที่เจตนาในการออกอากาศยังคงเป็นการออกแบบที่เหมาะสมที่สุด แนวทางปฏิบัติแนะนำบางส่วนที่ควรพิจารณามีดังนี้

  • หากเป็นไปได้ ให้ใช้ Intent.FLAG_RECEIVER_REGISTERED_ONLY เพื่อจำกัดการออกอากาศ ไปยังแอปที่กำลังทำงานอยู่ เช่น ACTION_SCREEN_ON ใช้การออกแบบนี้เพื่อหลีกเลี่ยงการปลุกแอป
  • หากเป็นไปได้ ให้ใช้ Intent.setPackage() หรือ Intent.setComponent() เพื่อกำหนดเป้าหมาย การออกอากาศในแอปที่เฉพาะเจาะจงที่สนใจ ตัวอย่างเช่น ACTION_MEDIA_BUTTON ใช้การออกแบบนี้เพื่อมุ่งเน้นที่การจัดการแอปปัจจุบัน การควบคุมการเล่น
  • หากเป็นไปได้ ให้กำหนดการออกอากาศเป็น <protected-broadcast> เพื่อป้องกันไม่ให้ แอปที่เป็นอันตรายแอบอ้างเป็นระบบปฏิบัติการ

Intent ในบริการของนักพัฒนาแอปที่เชื่อมโยงกับระบบ

บริการที่นักพัฒนาแอปตั้งใจจะขยายและผูกไว้กับระบบ เช่น บริการแบบนามธรรมอย่าง NotificationListenerService อาจตอบสนองต่อการดำเนินการ Intent จากระบบ บริการดังกล่าวควรเป็นไปตามเกณฑ์ต่อไปนี้

  1. กำหนดSERVICE_INTERFACEค่าคงที่สตริงในคลาสที่มี ชื่อคลาสของบริการแบบเต็ม ค่าคงที่นี้ต้องมีคำอธิบายประกอบ ด้วย @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
  2. เอกสารเกี่ยวกับคลาสที่นักพัฒนาแอปต้องเพิ่ม <intent-filter> ลงใน AndroidManifest.xml เพื่อรับ Intent จากแพลตฟอร์ม
  3. ขอแนะนำอย่างยิ่งให้เพิ่มสิทธิ์ระดับระบบเพื่อป้องกันไม่ให้แอปที่ไม่พึงประสงค์ ส่ง Intent ไปยังบริการของนักพัฒนาแอป

การทำงานร่วมกันของ Kotlin-Java

ดูรายการหลักเกณฑ์ทั้งหมดได้ในคำแนะนำเกี่ยวกับการทำงานร่วมกันของ Kotlin-Java อย่างเป็นทางการของ Android เราได้คัดลอกหลักเกณฑ์บางส่วนมาไว้ในคู่มือนี้เพื่อ ปรับปรุงการค้นพบ

ระดับการมองเห็น API

API ของ Kotlin บางรายการ เช่น suspend funs ไม่ได้มีไว้สำหรับนักพัฒนาซอฟต์แวร์ Java อย่างไรก็ตาม อย่าพยายามควบคุมระดับการมองเห็นเฉพาะภาษา โดยใช้ @JvmSynthetic เนื่องจากจะส่งผลข้างเคียงต่อวิธีที่ API แสดงใน ดีบักเกอร์ ซึ่งทำให้การแก้ไขข้อบกพร่องยากขึ้น

ดูคำแนะนำเฉพาะได้ที่คำแนะนำเกี่ยวกับการทำงานร่วมกันของ Kotlin-Java หรือคำแนะนำแบบไม่พร้อมกัน

ออบเจ็กต์ร่วม

Kotlin ใช้ companion object เพื่อแสดงสมาชิกแบบคงที่ ในบางกรณี ข้อมูลเหล่านี้จะแสดงจาก Java ในคลาสภายในชื่อ Companion แทนที่จะแสดงในคลาสที่มีข้อมูล Companion ชั้นเรียนอาจแสดงเป็นชั้นเรียนที่ว่างเปล่าในไฟล์ข้อความ API ซึ่งเป็นลักษณะการทำงานตามที่ตั้งใจไว้

หากต้องการเพิ่มความเข้ากันได้กับ Java ให้มากที่สุด ให้ใส่คำอธิบายประกอบฟิลด์ที่ไม่ใช่ค่าคงที่ของออบเจ็กต์คู่ด้วย @JvmField และฟังก์ชันสาธารณะด้วย @JvmStatic เพื่อแสดงฟิลด์และฟังก์ชันดังกล่าวโดยตรงในคลาสที่ประกอบด้วยฟิลด์และฟังก์ชันเหล่านั้น

companion object {
  @JvmField val BIG_INTEGER_ONE = BigInteger.ONE
  @JvmStatic fun fromPointF(pointf: PointF) {
    /* ... */
  }
}

วิวัฒนาการของ API แพลตฟอร์ม Android

ส่วนนี้อธิบายนโยบายเกี่ยวกับประเภทการเปลี่ยนแปลงที่คุณทำได้กับ Android API ที่มีอยู่ และวิธีที่คุณควรใช้การเปลี่ยนแปลงเหล่านั้นเพื่อเพิ่มความเข้ากันได้กับแอปและโค้ดเบสที่มีอยู่ให้ได้มากที่สุด

การเปลี่ยนแปลงที่ส่งผลกับไบนารี

หลีกเลี่ยงการเปลี่ยนแปลงที่ทำให้ไบนารีใช้งานไม่ได้ในพื้นผิว API สาธารณะที่เสร็จสมบูรณ์แล้ว โดยทั่วไปแล้ว การเปลี่ยนแปลงประเภทนี้จะทำให้เกิดข้อผิดพลาดเมื่อเรียกใช้ make update-api แต่ก็อาจมีกรณีที่ API ของ Metalava ตรวจไม่พบ หากไม่แน่ใจ โปรดอ่านคำแนะนำการพัฒนา API ที่ใช้ Java ของ Eclipse Foundation เพื่อดูคำอธิบายโดยละเอียดเกี่ยวกับประเภทการเปลี่ยนแปลง API ที่เข้ากันได้ใน Java การเปลี่ยนแปลงที่ทำให้ไบนารีใช้งานไม่ได้ใน API ที่ซ่อนอยู่ (เช่น API ของระบบ) ควรเป็นไปตามวงจรเลิกใช้งาน/แทนที่

การเปลี่ยนแปลงที่ทำให้แหล่งข้อมูลใช้งานไม่ได้

เราไม่แนะนำให้ทำการเปลี่ยนแปลงที่ทำให้แหล่งข้อมูลใช้งานไม่ได้ แม้ว่าการเปลี่ยนแปลงนั้นจะไม่ทำให้ไบนารีใช้งานไม่ได้ก็ตาม ตัวอย่างหนึ่งของการเปลี่ยนแปลงที่เข้ากันได้กับไบนารีแต่ทำให้แหล่งที่มาใช้งานไม่ได้คือการเพิ่ม Generics ลงในคลาสที่มีอยู่ ซึ่งเข้ากันได้กับไบนารีแต่ทำให้เกิดข้อผิดพลาดในการคอมไพล์เนื่องจากการรับค่าหรือการอ้างอิงที่ไม่ชัดเจน การเปลี่ยนแปลงที่ทำให้แหล่งที่มาใช้งานไม่ได้จะไม่ทำให้เกิดข้อผิดพลาดเมื่อเรียกใช้ make update-api ดังนั้น คุณต้องระมัดระวังเพื่อทำความเข้าใจผลกระทบของการเปลี่ยนแปลงต่อลายเซ็น API ที่มีอยู่

ในบางกรณี การเปลี่ยนแปลงที่ทำให้แหล่งที่มาใช้งานไม่ได้อาจจำเป็นต่อการปรับปรุงประสบการณ์ของนักพัฒนาซอฟต์แวร์ หรือความถูกต้องของโค้ด ตัวอย่างเช่น การเพิ่มคำอธิบายประกอบเกี่ยวกับค่า Null ให้กับแหล่งที่มาของ Java จะช่วยปรับปรุงการทำงานร่วมกันกับโค้ด Kotlin และลดโอกาสที่จะเกิดข้อผิดพลาด แต่ก็มักจะต้องมีการเปลี่ยนแปลงโค้ดแหล่งที่มา ซึ่งบางครั้งอาจเป็นการเปลี่ยนแปลงที่สำคัญ

การเปลี่ยนแปลง API ส่วนตัว

คุณเปลี่ยน API ที่มีคำอธิบายประกอบด้วย @TestApi ได้ทุกเมื่อ

คุณต้องเก็บรักษา API ที่มีคำอธิบายประกอบด้วย @SystemApi เป็นเวลา 3 ปี คุณต้อง นำออกหรือเปลี่ยนโครงสร้างภายในโค้ดของ System API ตามกำหนดการต่อไปนี้

  • API y - เพิ่มแล้ว
  • API y+1 - การเลิกใช้งาน
    • ทำเครื่องหมายโค้ดด้วย @Deprecated
    • เพิ่มการแทนที่และลิงก์ไปยังการแทนที่ใน Javadoc สำหรับโค้ดที่เลิกใช้งานแล้วโดยใช้คำอธิบายประกอบ @deprecated docs
    • ในระหว่างวงจรการพัฒนา ให้รายงานข้อบกพร่องเกี่ยวกับไฟล์ต่อผู้ใช้ภายในโดยแจ้งให้ทราบว่า API จะเลิกใช้งาน ซึ่งจะช่วยตรวจสอบว่า API ที่ใช้แทนนั้นเพียงพอ
  • API y+2 - การนำออกอย่างนุ่มนวล
    • ทำเครื่องหมายโค้ดด้วย @removed
    • ไม่บังคับ: ส่งข้อยกเว้นหรือไม่มีการดำเนินการสำหรับแอปที่กำหนดเป้าหมายเป็นระดับ SDK ปัจจุบัน สำหรับการเผยแพร่
  • API y+3 - การนำออกอย่างถาวร
    • นำโค้ดออกจากโครงสร้างแหล่งที่มาโดยสมบูรณ์

การเลิกใช้งาน

เราถือว่าการเลิกใช้งานเป็นการเปลี่ยนแปลง API และอาจเกิดขึ้นในรุ่นหลัก (เช่น ตัวอักษร) ใช้@Deprecatedคำอธิบายประกอบแหล่งที่มาและ@deprecated <summary>คำอธิบายประกอบเอกสารร่วมกันเมื่อเลิกใช้งาน API ข้อมูลสรุปต้อง มีกลยุทธ์การย้ายข้อมูล กลยุทธ์นี้อาจลิงก์ไปยัง API ที่มาแทนที่หรือ อธิบายเหตุผลที่คุณไม่ควรใช้ 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)

คุณต้องเลิกใช้งาน API ที่กำหนดไว้ใน XML และแสดงใน Java ด้วย ซึ่งรวมถึง แอตทริบิวต์และพร็อพเพอร์ตี้ที่กำหนดสไตล์ได้ซึ่งแสดงในคลาส android.R พร้อม สรุปดังนี้

<!-- Attribute whether the accessibility service ...
     {@deprecated Not used by the framework}
 -->
<attr name="canRequestEnhancedWebAccessibility" format="boolean" />

เมื่อใดที่ควรเลิกใช้งาน API

การเลิกใช้งานมีประโยชน์มากที่สุดในการไม่แนะนำให้ใช้ API ในโค้ดใหม่

นอกจากนี้ เรายังกำหนดให้คุณทำเครื่องหมาย API เป็น @deprecated ก่อนที่จะ@removed แต่การดำเนินการนี้ไม่ได้กระตุ้นให้นักพัฒนาแอปย้ายข้อมูลออกจาก API ที่ใช้อยู่แล้ว

โปรดพิจารณาผลกระทบที่มีต่อนักพัฒนาก่อนที่จะเลิกใช้งาน API การเลิกใช้งาน API จะส่งผลดังนี้

  • javac จะแสดงคำเตือนระหว่างการคอมไพล์
    • คุณไม่สามารถระงับคำเตือนการเลิกใช้งานทั่วโลกหรือกำหนดค่าพื้นฐานได้ ดังนั้น นักพัฒนาแอปที่ใช้ -Werror ต้องแก้ไขหรือระงับการใช้งาน API ที่เลิกใช้งานแล้วทุกรายการ ก่อนที่จะอัปเดต SDK เวอร์ชันที่ใช้คอมไพล์ได้
    • คุณไม่สามารถระงับคำเตือนการเลิกใช้งานเมื่อนำเข้าคลาสที่เลิกใช้งานแล้วได้ ด้วยเหตุนี้ นักพัฒนาแอปจึงต้องแทรกชื่อคลาสที่สมบูรณ์ สำหรับทุกการใช้งานคลาสที่เลิกใช้งานแล้วก่อนที่จะ อัปเดตเวอร์ชัน SDK ที่คอมไพล์ได้
  • เอกสารเกี่ยวกับ d.android.com แสดงประกาศการเลิกใช้งาน
  • IDE เช่น Android Studio จะแสดงคำเตือนที่เว็บไซต์การใช้งาน API
  • IDE อาจลดอันดับหรือซ่อน API จากการเติมข้อความอัตโนมัติ

ด้วยเหตุนี้ การเลิกใช้งาน API จึงอาจทำให้นักพัฒนาแอปที่กังวลเรื่องความสมบูรณ์ของโค้ดมากที่สุด (ผู้ที่ใช้ -Werror) ไม่ยอมรับ SDK ใหม่ นักพัฒนาซอฟต์แวร์ที่ไม่กังวลเกี่ยวกับคำเตือนในโค้ดที่มีอยู่มีแนวโน้มที่จะ ไม่สนใจการเลิกใช้งานไปเลย

SDK ที่ทำให้เกิดการเลิกใช้งานจำนวนมากจะทำให้ทั้ง 2 กรณีนี้แย่ลง

ด้วยเหตุนี้ เราจึงแนะนำให้เลิกใช้งาน API เฉพาะในกรณีต่อไปนี้

  • เราวางแผนที่จะ@remove API ในรุ่นต่อๆ ไป
  • การใช้ API ทำให้เกิดลักษณะการทำงานที่ไม่ถูกต้องหรือไม่แน่นอน ซึ่งเราไม่สามารถแก้ไขได้โดยไม่ ทำให้เกิดปัญหาความเข้ากันได้

เมื่อเลิกใช้งาน API และแทนที่ด้วย API ใหม่ เราขอแนะนำเป็นอย่างยิ่ง ให้เพิ่ม API ความเข้ากันได้ที่เกี่ยวข้องลงในไลบรารี Jetpack เช่น androidx.core เพื่อให้รองรับทั้งอุปกรณ์เก่าและใหม่ได้ง่ายขึ้น

เราไม่แนะนำให้เลิกใช้งาน API ที่ทำงานได้ตามที่ตั้งใจไว้ในรุ่นปัจจุบันและรุ่นต่อๆ ไป

/**
 * ...
 * @deprecated Use {@link #doThing(int, Bundle)} instead.
 */
@Deprecated
public void doThing(int action) {
  ...
}

public void doThing(int action, @Nullable Bundle extras) {
  ...
}

การเลิกใช้งานจะเหมาะสมในกรณีที่ API ไม่สามารถรักษาลักษณะการทำงานที่ระบุไว้ได้อีกต่อไป

/**
 * ...
 * @deprecated No longer displayed in the status bar as of API 21.
 */
@Deprecated
public RemoteViews tickerView;

การเปลี่ยนแปลง API ที่เลิกใช้งานแล้ว

คุณต้องรักษารูปแบบการทำงานของ API ที่เลิกใช้งานแล้ว ซึ่งหมายความว่าการติดตั้งใช้งานทดสอบ ต้องยังคงเหมือนเดิม และการทดสอบต้องผ่านต่อไปหลังจากที่คุณ เลิกใช้งาน API แล้ว หาก API ไม่มีการทดสอบ คุณควรเพิ่มการทดสอบ

อย่าขยายพื้นผิว API ที่เลิกใช้งานแล้วในรุ่นต่อๆ ไป คุณสามารถเพิ่มคำอธิบายประกอบความถูกต้องของ Lint (เช่น @Nullable) ลงใน API ที่เลิกใช้งานแล้วที่มีอยู่ ได้ แต่ไม่ควรเพิ่ม API ใหม่

อย่าเพิ่ม API ใหม่เป็น API ที่เลิกใช้งานแล้ว หากมีการเพิ่ม API ใดๆ และต่อมามีการเลิกใช้ภายในรอบการเผยแพร่รุ่นก่อนเปิดตัว (ดังนั้น API จะเข้าสู่พื้นผิว API สาธารณะในตอนแรกในสถานะเลิกใช้แล้ว) คุณต้องนำ API เหล่านั้นออกก่อนที่จะสรุป API

การนำออกชั่วคราว

การนำออกอย่างไม่สมบูรณ์เป็นการเปลี่ยนแปลงที่ทำให้แหล่งที่มาใช้งานไม่ได้ และคุณควรหลีกเลี่ยงใน API สาธารณะ เว้นแต่สภา API จะอนุมัติอย่างชัดเจน สำหรับ API ของระบบ คุณต้องเลิกใช้งาน API เป็นระยะเวลาเท่ากับการเปิดตัวเวอร์ชันหลักก่อนที่จะนำออกอย่างนุ่มนวล นำการอ้างอิงเอกสารทั้งหมดไปยัง API ออก และใช้คำอธิบายประกอบเอกสาร @removed <summary> เมื่อนำ API ออกชั่วคราว สรุปของคุณต้องระบุเหตุผลในการนำออกและอาจระบุกลยุทธ์การย้ายข้อมูลได้ตามที่เราอธิบายไว้ในการเลิกใช้งาน

ลักษณะการทำงานของ API ที่นำออกอย่างไม่ถาวรสามารถคงไว้ได้ตามเดิม แต่ที่สำคัญกว่านั้นคือต้องคงไว้เพื่อให้ผู้เรียกใช้ที่มีอยู่ไม่ขัดข้องเมื่อเรียกใช้ API ในบางกรณี นั่นอาจหมายถึงการรักษาลักษณะการทำงาน

คุณต้องรักษาความครอบคลุมของการทดสอบไว้ แต่เนื้อหาของการทดสอบอาจต้อง เปลี่ยนแปลงเพื่อให้สอดคล้องกับการเปลี่ยนแปลงด้านพฤติกรรม การทดสอบยังคงต้องตรวจสอบว่าผู้โทรที่มีอยู่ไม่ขัดข้องในขณะรันไทม์ คุณสามารถรักษาลักษณะการทำงานของ API ที่ถูกนำออกอย่างไม่สมบูรณ์ไว้ได้ แต่ที่สำคัญกว่านั้นคือคุณต้องรักษาลักษณะการทำงานดังกล่าวไว้เพื่อไม่ให้ผู้เรียกใช้ที่มีอยู่เดิมเกิดข้อขัดข้องเมื่อเรียกใช้ API ในบางกรณี นั่นอาจหมายถึงการรักษาลักษณะการทำงานไว้

คุณต้องรักษาความครอบคลุมของการทดสอบ แต่เนื้อหาของการทดสอบอาจต้อง เปลี่ยนแปลงเพื่อให้สอดคล้องกับการเปลี่ยนแปลงด้านพฤติกรรม การทดสอบยังคงต้องตรวจสอบว่าผู้โทรที่มีอยู่ไม่ขัดข้องในขณะรันไทม์

ในระดับเทคนิค เราจะนำ API ออกจาก JAR ของ SDK Stub และ classpath เวลาคอมไพล์โดยใช้คำอธิบายประกอบ @remove Javadoc แต่ API จะยังคงอยู่ใน classpath รันไทม์ ซึ่งคล้ายกับ API ของ @hide

/**
 * Ringer volume. This is ...
 *
 * @removed Not functional since API 2.
 */
public static final String VOLUME_RING = ...

ในมุมมองของนักพัฒนาแอป API จะไม่ปรากฏในการเติมข้อความอัตโนมัติ และซอร์สโค้ดที่อ้างอิง API จะคอมไพล์ไม่ได้เมื่อ compileSdk มีเวอร์ชัน เท่ากับหรือใหม่กว่า SDK ที่นำ API ออก อย่างไรก็ตาม ซอร์สโค้ด จะยังคงคอมไพล์กับ SDK เวอร์ชันก่อนหน้าได้สำเร็จ และไบนารีที่ อ้างอิง API จะยังคงทำงานได้

ห้ามนำ API บางหมวดหมู่ออกโดยไม่แจ้งให้ทราบ คุณต้องไม่ลบแบบไม่สมบูรณ์ API บางหมวดหมู่

วิธีการนามธรรม

คุณต้องไม่นำเมธอดนามธรรมในคลาสที่นักพัฒนาแอปอาจขยายออกไป ออกอย่างไม่ถาวร การทำเช่นนี้จะทำให้นักพัฒนาแอปขยายคลาสใน SDK ทุกระดับไม่สำเร็จ

ในกรณีที่เกิดขึ้นไม่บ่อยนักซึ่งนักพัฒนาแอปไม่เคยและจะไม่สามารถขยายคลาสได้ คุณยังคงนำเมธอดนามธรรมออกอย่างไม่ถาวรได้

การนำออกอย่างถาวร

การนำออกอย่างถาวรเป็นการเปลี่ยนแปลงที่ทำให้เกิดการหยุดทำงานแบบไบนารี และไม่ควรเกิดขึ้นใน API สาธารณะ

คำอธิบายประกอบที่ไม่แนะนำ

เราใช้คำอธิบายประกอบ @Discouraged เพื่อระบุว่าเราไม่แนะนำให้ใช้ API ในกรณีส่วนใหญ่ (>95%) API ที่ไม่แนะนําแตกต่างจาก API ที่เลิกใช้งานแล้วตรงที่ มีกรณีการใช้งานที่สําคัญและเฉพาะเจาะจงซึ่งทําให้ไม่สามารถเลิกใช้งานได้ เมื่อทำเครื่องหมาย API ว่าไม่แนะนำ คุณต้องระบุคำอธิบายและโซลูชันทางเลือก

@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);
}

คุณไม่ควรเพิ่ม API ใหม่ตามที่ระบบไม่แนะนำ

การเปลี่ยนแปลงลักษณะการทำงานของ API ที่มีอยู่

ในบางกรณี คุณอาจต้องการเปลี่ยนลักษณะการทำงานของการติดตั้งใช้งาน API ที่มีอยู่ ตัวอย่างเช่น ใน Android 7.0 เราได้ปรับปรุง DropBoxManager เพื่อ สื่อสารอย่างชัดเจนเมื่อนักพัฒนาซอฟต์แวร์พยายามโพสต์เหตุการณ์ที่มีขนาดใหญ่เกินกว่าจะ ส่งผ่าน Binder

อย่างไรก็ตาม เพื่อหลีกเลี่ยงการทำให้เกิดปัญหาสำหรับแอปที่มีอยู่ เราขอแนะนำอย่างยิ่งให้ รักษาลักษณะการทำงานที่ปลอดภัยสำหรับแอปเวอร์ชันเก่า ที่ผ่านมา เราได้ป้องกันการเปลี่ยนแปลงลักษณะการทำงานเหล่านี้ตาม ApplicationInfo.targetSdkVersion ของแอป แต่ เมื่อเร็วๆ นี้เราได้ย้ายข้อมูลไปกำหนดให้ใช้ App Compatibility 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
    }
  }
}

การใช้การออกแบบ App Compatibility Framework นี้ช่วยให้นักพัฒนาแอปปิดใช้การเปลี่ยนแปลงลักษณะการทำงานที่เฉพาะเจาะจงชั่วคราวได้ในระหว่างการเปิดตัวเวอร์ชันตัวอย่างและเบต้า ซึ่งเป็นส่วนหนึ่งของการแก้ไขข้อบกพร่องของแอป แทนที่จะบังคับให้นักพัฒนาแอปปรับเปลี่ยนลักษณะการทำงานหลายสิบอย่างพร้อมกัน

ความเข้ากันได้กับรุ่นถัดไป

ความเข้ากันได้แบบย้อนกลับเป็นลักษณะการออกแบบที่ช่วยให้ระบบยอมรับ อินพุตที่ตั้งใจใช้กับระบบเวอร์ชันที่ใหม่กว่า ในกรณีของการออกแบบ API คุณ ต้องให้ความสนใจเป็นพิเศษกับการออกแบบเริ่มต้นและการเปลี่ยนแปลงในอนาคต เนื่องจากนักพัฒนาซอฟต์แวร์คาดหวังที่จะเขียนโค้ดเพียงครั้งเดียว ทดสอบเพียงครั้งเดียว และให้โค้ดทำงาน ได้ทุกที่โดยไม่มีปัญหา

สาเหตุที่พบบ่อยที่สุดของปัญหาความเข้ากันได้แบบย้อนกลับใน Android มีดังนี้

  • การเพิ่มค่าคงที่ใหม่ลงในชุด (เช่น @IntDef หรือ enum) ที่ก่อนหน้านี้ ถือว่าสมบูรณ์แล้ว (เช่น ในกรณีที่ switch มี default ที่ ทำให้เกิดข้อยกเว้น)
  • การเพิ่มการรองรับฟีเจอร์ที่ไม่ได้บันทึกโดยตรงในพื้นผิว API (เช่น การรองรับการกำหนดทรัพยากรประเภท ColorStateList ใน XML ซึ่งก่อนหน้านี้รองรับเฉพาะทรัพยากร <color>)
  • การผ่อนปรนข้อจำกัดในการตรวจสอบรันไทม์ เช่น การนำการตรวจสอบ requireNotNull() ออกในเวอร์ชันที่ต่ำกว่า

ในกรณีเหล่านี้ นักพัฒนาแอปจะทราบว่ามีข้อผิดพลาดเมื่อรันไทม์เท่านั้น ที่แย่กว่านั้นคือผู้ใช้ดังกล่าวอาจทราบเรื่องนี้จากรายงานข้อขัดข้องจากอุปกรณ์รุ่นเก่า ในภาคสนาม

นอกจากนี้ กรณีเหล่านี้ยังเป็นการเปลี่ยนแปลง API ที่ถูกต้องตามเทคนิคทั้งหมด โดยจะไม่ ทําให้ความเข้ากันได้ของไบนารีหรือแหล่งที่มาเสียหาย และโปรแกรมตรวจสอบ API จะไม่พบปัญหาเหล่านี้

ด้วยเหตุนี้ ผู้ออกแบบ API จึงต้องใส่ใจเป็นพิเศษเมื่อแก้ไขคลาสที่มีอยู่ ถามว่า "การเปลี่ยนแปลงนี้จะทำให้โค้ดที่เขียนและทดสอบเฉพาะกับแพลตฟอร์มเวอร์ชันล่าสุดทำงานล้มเหลวในเวอร์ชันที่ต่ำกว่าไหม"

สคีมา XML

หากสคีมา XML ทำหน้าที่เป็นอินเทอร์เฟซที่เสถียรระหว่างคอมโพเนนต์ คุณต้องระบุสคีมานั้นอย่างชัดเจนและต้องพัฒนาในลักษณะที่เข้ากันได้แบบย้อนหลัง เช่นเดียวกับ Android API อื่นๆ ตัวอย่างเช่น โครงสร้างขององค์ประกอบ XML และแอตทริบิวต์ต้องยังคงอยู่เช่นเดียวกับวิธีที่รักษาเมธอดและตัวแปรในพื้นผิว API อื่นๆ ของ Android

การเลิกใช้งาน XML

หากต้องการเลิกใช้งานองค์ประกอบหรือแอตทริบิวต์ XML คุณสามารถเพิ่มเครื่องหมาย xs:annotation ได้ แต่คุณต้องรองรับไฟล์ XML ที่มีอยู่ต่อไป โดยทำตามวงจรการพัฒนา@SystemApiตามปกติ

<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>

ต้องคงประเภทองค์ประกอบไว้

สคีมารองรับองค์ประกอบ sequence, องค์ประกอบ choice และองค์ประกอบ all เป็น องค์ประกอบย่อยขององค์ประกอบ complexType อย่างไรก็ตาม องค์ประกอบย่อยเหล่านี้แตกต่างกันใน จำนวนและลำดับขององค์ประกอบย่อย ดังนั้นการแก้ไขประเภทที่มีอยู่ จึงเป็นการเปลี่ยนแปลงที่ไม่เข้ากัน

หากต้องการแก้ไขประเภทที่มีอยู่ แนวทางปฏิบัติแนะนำคือการเลิกใช้งานประเภทเก่าและเปิดตัวประเภทใหม่เพื่อแทนที่

<!-- 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 เป็นโปรเจ็กต์ที่อนุญาตให้อัปเดต ระบบย่อย ("โมดูล Mainline") ของระบบปฏิบัติการ Android ทีละรายการ แทนที่จะ อัปเดตรูปภาพระบบทั้งหมด

โมดูล Mainline ต้อง "แยก" ออกจากแพลตฟอร์มหลัก ซึ่งหมายความว่าการโต้ตอบทั้งหมด ระหว่างแต่ละโมดูลกับส่วนอื่นๆ ของโลกต้องดำเนินการ โดยใช้ API อย่างเป็นทางการ (สาธารณะหรือระบบ)

โมดูล Mainline บางโมดูลควรปฏิบัติตามรูปแบบการออกแบบบางอย่าง ส่วนนี้ จะอธิบายถึงการดำเนินการดังกล่าว

รูปแบบ <Module>FrameworkInitializer

หากโมดูล Mainline ต้องเปิดเผยคลาส @SystemService (เช่น JobScheduler) ให้ใช้รูปแบบต่อไปนี้

  • เปิดเผยคลาส <YourModule>FrameworkInitializer จากโมดูล คลาสนี้ต้องอยู่ใน $BOOTCLASSPATH ตัวอย่าง StatsFrameworkInitializer

  • ทำเครื่องหมายด้วย @SystemApi(client = MODULE_LIBRARIES)

  • เพิ่มpublic static void registerServiceWrappers()ลงในรายการ

  • ใช้ SystemServiceRegistry.registerContextAwareService() เพื่อลงทะเบียนคลาส Service Manager เมื่อต้องการอ้างอิงถึง Context

  • ใช้ SystemServiceRegistry.registerStaticService() เพื่อลงทะเบียนคลาส Service Manager เมื่อไม่จำเป็นต้องมีการอ้างอิงถึง Context

  • เรียกใช้เมธอด registerServiceWrappers() จาก ตัวเริ่มต้นแบบคงที่ของ SystemServiceRegistry

รูปแบบ <Module>ServiceManager

โดยปกติแล้ว หากต้องการลงทะเบียนออบเจ็กต์ Binder ของบริการระบบหรือรับการอ้างอิง ถึงออบเจ็กต์เหล่านั้น ผู้ใช้จะต้องใช้ ServiceManager แต่โมดูล Mainline จะใช้ไม่ได้เนื่องจากซ่อนอยู่ คลาสนี้ซ่อนอยู่ เนื่องจากโมดูลหลักไม่ควรลงทะเบียนหรืออ้างอิงออบเจ็กต์ Binder ของบริการระบบ ที่แพลตฟอร์มแบบคงที่หรือโมดูลอื่นๆ แสดง

โมดูล Mainline สามารถใช้รูปแบบต่อไปนี้แทนเพื่อลงทะเบียน และรับการอ้างอิงไปยังบริการ Binder ที่ติดตั้งใช้งานภายในโมดูลได้

  • สร้าง<YourModule>ServiceManagerคลาสตามการออกแบบของ TelephonyServiceManager

  • เปิดเผยชั้นเรียนเป็น @SystemApi หากต้องการเข้าถึงจาก $BOOTCLASSPATH คลาสหรือคลาสเซิร์ฟเวอร์ของระบบเท่านั้น คุณสามารถใช้ @SystemApi(client = MODULE_LIBRARIES)ได้ มิฉะนั้น @SystemApi(client = PRIVILEGED_APPS) จะใช้งานได้

  • ชั้นเรียนนี้ประกอบด้วย

    • ตัวสร้างที่ซ่อนอยู่ เพื่อให้มีเพียงโค้ดแพลตฟอร์มแบบคงที่เท่านั้นที่สร้างอินสแตนซ์ได้
    • เมธอด Getter สาธารณะที่แสดงผลอินสแตนซ์ ServiceRegisterer สำหรับชื่อที่เฉพาะเจาะจง หากมีออบเจ็กต์ Binder 1 รายการ คุณจะต้องมีเมธอด Getter 1 รายการ หากมี 2 รายการ คุณก็ต้องมีตัวรับ 2 รายการ
    • ใน ActivityThread.initializeMainlineModules() ให้สร้างอินสแตนซ์ของคลาสนี้ แล้วส่งไปยังเมธอดแบบคงที่ที่โมดูลของคุณ แสดง โดยปกติแล้ว คุณจะเพิ่ม @SystemApi(client = MODULE_LIBRARIES) API แบบคงที่ในคลาส FrameworkInitializer ที่ใช้ API ดังกล่าว

รูปแบบนี้จะป้องกันไม่ให้โมดูลหลักอื่นๆ เข้าถึง API เหล่านี้ เนื่องจากไม่มีวิธีที่โมดูลอื่นๆ จะรับอินสแตนซ์ของ <YourModule>ServiceManager แม้ว่า API ของ get() และ register() จะ มองเห็นได้ก็ตาม

ต่อไปนี้คือวิธีที่โทรศัพท์อ้างอิงถึงบริการโทรศัพท์ ลิงก์การค้นหาโค้ด

หากคุณใช้ออบเจ็กต์ Service Binder ในโค้ดเนทีฟ คุณจะใช้AServiceManager API เนทีฟ API เหล่านี้สอดคล้องกับ ServiceManager Java API แต่ API ดั้งเดิมจะ แสดงต่อโมดูลหลักโดยตรง อย่าใช้เพื่อลงทะเบียนหรืออ้างอิงถึง ออบเจ็กต์ Binder ที่โมดูลของคุณไม่ได้เป็นเจ้าของ หากคุณแสดงออบเจ็กต์ Binder จากโค้ดเนทีฟ <YourModule>ServiceManager.ServiceRegisterer ไม่จำเป็นต้องมีเมธอด register()

คำจำกัดความของสิทธิ์ในโมดูลเมนไลน์

โมดูล Mainline ที่มี APK อาจกำหนดสิทธิ์ (ที่กำหนดเอง) ใน APK AndroidManifest.xml ในลักษณะเดียวกับ APK ปกติ

หากใช้สิทธิ์ที่กำหนดไว้ภายในโมดูลเท่านั้น ชื่อสิทธิ์ควรมีคำนำหน้าเป็นชื่อแพ็กเกจ APK เช่น

<permission
    android:name="com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER"
    android:protectionLevel="signature" />

หากต้องระบุสิทธิ์ที่กำหนดเป็นส่วนหนึ่งของ API แพลตฟอร์มที่อัปเดตได้ ให้กับแอปอื่นๆ ชื่อสิทธิ์ควรมีคำนำหน้าเป็น "android.permission." (เช่น สิทธิ์ของแพลตฟอร์มแบบคงที่) รวมถึงชื่อแพ็กเกจของโมดูล เพื่อส่งสัญญาณว่า เป็น API ของแพลตฟอร์มจากโมดูล ในขณะที่หลีกเลี่ยงการตั้งชื่อที่ซ้ำกัน เช่น

<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" />

จากนั้นโมดูลจะแสดงชื่อสิทธิ์นี้เป็นค่าคงที่ของ API ใน API surface เช่น HealthPermissions.READ_ACTIVE_CALORIES_BURNED