ชั้นข้อมูล

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

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

สถาปัตยกรรมชั้นข้อมูล

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

วันที่ ในสถาปัตยกรรมทั่วไป ที่เก็บของชั้นข้อมูลจะให้ข้อมูล
    ส่วนที่เหลือของแอปและขึ้นอยู่กับแหล่งข้อมูล
รูปที่ 1 บทบาทของชั้นข้อมูลในสถาปัตยกรรมแอป

คลาสที่เก็บมีหน้าที่รับผิดชอบงานต่อไปนี้:

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

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

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

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

ทำตามแนวทางปฏิบัติแนะนำเกี่ยวกับการแทรกทรัพยากร Dependency ที่เก็บจะใช้แหล่งข้อมูลเป็นทรัพยากร Dependency ในตัวสร้าง:

class ExampleRepository(
    private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
    private val exampleLocalDataSource: ExampleLocalDataSource // database
) { /* ... */ }

แสดง API

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

  • การดำเนินการแบบครั้งเดียว: ชั้นข้อมูลควรแสดงฟังก์ชันระงับใน Kotlin; และสำหรับภาษาโปรแกรม Java ชั้นข้อมูลควรเปิดเผย ฟังก์ชันที่มี Callback เพื่อแจ้งผลของการดำเนินการ หรือ RxJava Single, Maybe หรือ Completable
  • รับการแจ้งเตือนเกี่ยวกับการเปลี่ยนแปลงของข้อมูลเมื่อเวลาผ่านไป: ชั้นข้อมูลควรแสดง flows ใน Kotlin และสำหรับภาษาโปรแกรม Java ค่า ชั้นข้อมูลควรแสดง Callback ที่ปล่อยข้อมูลใหม่ หรือ RxJava ประเภท Observable หรือ Flowable
class ExampleRepository(
    private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
    private val exampleLocalDataSource: ExampleLocalDataSource // database
) {

    val data: Flow<Example> = ...

    suspend fun modifyData(example: Example) { ... }
}

รูปแบบการตั้งชื่อในคู่มือนี้

ในคู่มือนี้ คลาสที่เก็บจะตั้งชื่อตามข้อมูลที่ เป็นผู้รับผิดชอบ ซึ่งมีรายละเอียดดังนี้

type of data + Repository

เช่น NewsRepository, MoviesRepository หรือ PaymentsRepository

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

ประเภทข้อมูล + ประเภทแหล่งข้อมูล + แหล่งข้อมูล

สำหรับประเภทข้อมูล ให้ใช้ระยะไกล หรือในเครื่อง เพื่อให้มีความเฉพาะเจาะจงมากขึ้นเนื่องจาก การติดตั้งใช้งานอาจเปลี่ยนแปลงได้ เช่น NewsRemoteDataSource หรือ NewsLocalDataSource เพื่อให้มีความเฉพาะเจาะจงมากขึ้นในกรณีที่แหล่งข้อมูลสําคัญ ให้ใช้ ประเภทของแหล่งที่มา เช่น NewsNetworkDataSource หรือ NewsDiskDataSource

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

ที่เก็บหลายระดับ

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

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

วันที่ ในตัวอย่างนี้ UserRepository ขึ้นอยู่กับคลาสที่เก็บอื่นอีก 2 คลาส:
    LockRepository ซึ่งขึ้นอยู่กับแหล่งข้อมูลการเข้าสู่ระบบอื่นๆ และ
    RegistrationRepository ซึ่งขึ้นอยู่กับแหล่งข้อมูลการลงทะเบียนอื่นๆ
รูปที่ 2 กราฟการขึ้นต่อกันของที่เก็บที่ขึ้นอยู่กับ ที่เก็บได้

แหล่งที่มาของความจริง

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

แหล่งข้อมูลที่ถูกต้องอาจเป็นแหล่งข้อมูล เช่น ฐานข้อมูล หรือแม้แต่ข้อมูล แคชในหน่วยความจำที่ที่เก็บอาจมีอยู่ รวมที่เก็บ แหล่งข้อมูลต่างๆ และแก้ไขข้อขัดแย้งที่อาจเกิดขึ้นระหว่างข้อมูล แหล่งที่ต้องอัปเดตแหล่งข้อมูลที่ถูกต้องแห่งเดียวเป็นประจำหรือเนื่องจากข้อมูลจากผู้ใช้ กิจกรรม

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

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

ด้ายกำจัดขน

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

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

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

วงจร

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

หากคลาสมีข้อมูลในหน่วยความจำ เช่น แคช คุณอาจต้องการใช้ซ้ำ อินสแตนซ์เดียวกันของชั้นเรียนนั้นในระยะเวลาหนึ่งๆ และนี่ยัง ซึ่งเรียกว่าวงจรของอินสแตนซ์คลาส

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

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

นำเสนอโมเดลธุรกิจ

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

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

data class ArticleApiModel(
    val id: Long,
    val title: String,
    val content: String,
    val publicationDate: Date,
    val modifications: Array<ArticleApiModel>,
    val comments: Array<CommentApiModel>,
    val lastModificationDate: Date,
    val authorId: Long,
    val authorName: String,
    val authorDateOfBirth: Date,
    val readTimeMin: Int
)

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

data class Article(
    val id: Long,
    val title: String,
    val content: String,
    val publicationDate: Date,
    val authorName: String,
    val readTimeMin: Int
)

การแยกคลาสโมเดลมีประโยชน์ดังนี้

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

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

ประเภทของการดำเนินการข้อมูล

ชั้นข้อมูลจะจัดการกับประเภทของการดำเนินการที่แตกต่างกันไปตามระดับของการดำเนินการ ได้แก่ การดำเนินการที่มุ่งเน้น UI, การมุ่งเน้นแอป และการดำเนินธุรกิจ

การดำเนินการที่เน้น UI

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

โดยทั่วไปแล้ว การดำเนินการที่เน้น UI จะทริกเกอร์โดยเลเยอร์ UI และปฏิบัติตาม วงจรของผู้โทร เช่น วงจรของ ViewModel โปรดดูช่อง Make a คำขอเครือข่ายสำหรับตัวอย่างของ UI

การดำเนินการที่เน้นแอป

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

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

การดําเนินธุรกิจ

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

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

แสดงข้อผิดพลาด

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

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

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

งานทั่วไป

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

ส่งคำขอเครือข่าย

การส่งคำขอเครือข่ายเป็นงานที่พบบ่อยที่สุดอย่างหนึ่งที่แอป Android อาจ แสดง แอป News ต้องนำเสนอข่าวล่าสุดแก่ผู้ใช้ ดึงข้อมูลจากเครือข่าย ดังนั้นแอปจึงต้องมีคลาสแหล่งข้อมูลเพื่อจัดการ การทำงานของเครือข่าย: NewsRemoteDataSource หากต้องการเปิดเผยข้อมูลแก่ ที่เก็บใหม่ซึ่งจัดการการดำเนินการเกี่ยวกับข้อมูลข่าวสาร สร้างเมื่อ: NewsRepository

ข้อกำหนดก็คือคุณต้องอัปเดตข่าวล่าสุดเสมอเมื่อผู้ใช้ จะเปิดหน้าจอ ดังนั้นจึงเป็นการดำเนินการที่มุ่งเน้น UI

สร้างแหล่งข้อมูล

แหล่งข้อมูลต้องแสดงฟังก์ชันที่แสดงข่าวสารล่าสุด ซึ่งก็คือ "รายการ" จากอินสแตนซ์ ArticleHeadline รายการ แหล่งข้อมูลต้องระบุวิธีที่ปลอดภัยหลัก เพื่อรับข่าวสารล่าสุดจากเครือข่าย สำหรับกรณีนี้ เราจำเป็นต้อง การอ้างอิง CoroutineDispatcher หรือ Executor เพื่อเรียกใช้งาน

การส่งคำขอเครือข่ายเป็นการโทรแบบครั้งเดียวซึ่งจัดการโดย fetchLatestNews() ใหม่ วิธีการ:

class NewsRemoteDataSource(
  private val newsApi: NewsApi,
  private val ioDispatcher: CoroutineDispatcher
) {
    /**
     * Fetches the latest news from the network and returns the result.
     * This executes on an IO-optimized thread pool, the function is main-safe.
     */
    suspend fun fetchLatestNews(): List<ArticleHeadline> =
        // Move the execution to an IO-optimized thread since the ApiService
        // doesn't support coroutines and makes synchronous requests.
        withContext(ioDispatcher) {
            newsApi.fetchLatestNews()
        }
    }

// Makes news-related network synchronous requests.
interface NewsApi {
    fun fetchLatestNews(): List<ArticleHeadline>
}

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

สร้างที่เก็บ

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

// NewsRepository is consumed from other layers of the hierarchy.
class NewsRepository(
    private val newsRemoteDataSource: NewsRemoteDataSource
) {
    suspend fun fetchLatestNews(): List<ArticleHeadline> =
        newsRemoteDataSource.fetchLatestNews()
}

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

ใช้การแคชข้อมูลในหน่วยความจำ

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

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

แคช

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

แคชผลลัพธ์ของคำขอเครือข่าย

เพื่อความง่าย NewsRepository จะใช้ตัวแปรที่เปลี่ยนแปลงได้เพื่อแคชรายการล่าสุด ข่าว วิธีปกป้องการอ่านและเขียนจากชุดข้อความต่างๆ Mutex ดูข้อมูลเพิ่มเติมเกี่ยวกับสถานะที่เปลี่ยนแปลงได้และการเกิดขึ้นพร้อมกันที่แชร์ได้ที่ คอตลิน เอกสารประกอบ

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

class NewsRepository(
  private val newsRemoteDataSource: NewsRemoteDataSource
) {
    // Mutex to make writes to cached values thread-safe.
    private val latestNewsMutex = Mutex()

    // Cache of the latest news got from the network.
    private var latestNews: List<ArticleHeadline> = emptyList()

    suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
        if (refresh || latestNews.isEmpty()) {
            val networkResult = newsRemoteDataSource.fetchLatestNews()
            // Thread-safe write to latestNews
            latestNewsMutex.withLock {
                this.latestNews = networkResult
            }
        }

        return latestNewsMutex.withLock { this.latestNews }
    }
}

ทำให้การดำเนินการเกิดขึ้นนานกว่าหน้าจอ

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

หากต้องการทำตามแนวทางปฏิบัติแนะนำสำหรับการแทรกทรัพยากร Dependency NewsRepository ควรได้รับ เป็นพารามิเตอร์ในตัวสร้าง แทนที่จะสร้างพารามิเตอร์ของตัวเอง CoroutineScope เนื่องจากที่เก็บควรทำงานส่วนใหญ่ ชุดข้อความพื้นหลัง คุณควรกำหนดค่า CoroutineScope ด้วย Dispatchers.Default หรือพร้อมกลุ่มชุดข้อความของคุณเอง

class NewsRepository(
    ...,
    // This could be CoroutineScope(SupervisorJob() + Dispatchers.Default).
    private val externalScope: CoroutineScope
) { ... }

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

class NewsRepository(
    private val newsRemoteDataSource: NewsRemoteDataSource,
    private val externalScope: CoroutineScope
) {
    /* ... */

    suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
        return if (refresh) {
            externalScope.async {
                newsRemoteDataSource.fetchLatestNews().also { networkResult ->
                    // Thread-safe write to latestNews.
                    latestNewsMutex.withLock {
                        latestNews = networkResult
                    }
                }
            }.await()
        } else {
            return latestNewsMutex.withLock { this.latestNews }
        } 
    }
}

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

ดูบล็อกนี้ โพสต์ เพื่อดูข้อมูลเพิ่มเติมเกี่ยวกับรูปแบบสำหรับ CoroutineScope

บันทึกและเรียกข้อมูลจากดิสก์

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

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

  • สำหรับชุดข้อมูลขนาดใหญ่ที่ต้องค้นหา ต้องมีความสมบูรณ์จากการอ้างอิง หรือ ต้องอัปเดตบางส่วน ให้บันทึกข้อมูลไว้ในฐานข้อมูลของห้อง ในแอป News เช่น บทความหรือผู้เขียนข่าวอาจถูกบันทึกไว้ในฐานข้อมูล
  • สำหรับชุดข้อมูลขนาดเล็กที่จำเป็นต้องดึงข้อมูลและตั้งค่าเท่านั้น (ไม่ใช่คำค้นหาหรือ อัปเดตบางส่วน) ให้ใช้ DataStore ในตัวอย่างแอป News เนื้อหา รูปแบบวันที่ที่ต้องการหรือค่ากำหนดการแสดงผลอื่นๆ อาจบันทึกอยู่ใน DataStore
  • สำหรับกลุ่มข้อมูล เช่น ออบเจ็กต์ JSON ให้ใช้ไฟล์

ดังที่กล่าวไว้ในส่วนแหล่งข้อมูลที่เชื่อถือได้ ข้อมูลแต่ละรายการ แหล่งที่มาใช้ได้กับแหล่งข้อมูลเพียงแหล่งเดียวและสอดคล้องกับประเภทข้อมูลที่เจาะจง (สำหรับ เช่น News, Authors, NewsAndAuthors หรือ UserPreferences) คลาส ที่ใช้แหล่งข้อมูลไม่ควรทราบวิธีบันทึกข้อมูล ตัวอย่างเช่น ฐานข้อมูลหรือในไฟล์

ใช้ห้องแชทเป็นแหล่งข้อมูล

เนื่องจากแหล่งข้อมูลแต่ละแหล่งควรมีหน้าที่รับผิดชอบในการทำงานด้วยแหล่งข้อมูลเพียงแหล่งเดียว สำหรับประเภทข้อมูลเฉพาะ แหล่งข้อมูลของห้องพักจะได้รับ ออบเจ็กต์การเข้าถึงข้อมูล (DAO) หรือ ฐานข้อมูลตัวเองเป็นพารามิเตอร์ได้ ตัวอย่างเช่น NewsLocalDataSource อาจใช้เวลา อินสแตนซ์ของ NewsDao เป็นพารามิเตอร์ และ AuthorsLocalDataSource อาจใช้ อินสแตนซ์ของ AuthorsDao

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

หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับการทำงานกับ API ของห้อง โปรดดูห้อง

DataStore เป็นแหล่งข้อมูล

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

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

เช่น คุณอาจมี NotificationsDataStore ที่จัดการเฉพาะ ค่ากำหนดเกี่ยวกับการแจ้งเตือน และ NewsPreferencesDataStore ที่ จัดการค่ากำหนดที่เกี่ยวข้องกับหน้าจอข่าว ด้วยวิธีนี้ คุณจะสามารถกำหนดขอบเขต ทำให้ระบบอัปเดตดีขึ้น เพราะขั้นตอน newsScreenPreferencesDataStore.data เพียงอย่างเดียว จะปรากฏขึ้นเมื่อมีการเปลี่ยนแปลงค่ากำหนดที่เกี่ยวข้องกับหน้าจอนั้น และยังหมายความว่า วงจรของวัตถุอาจสั้นกว่านี้ เพราะวัตถุจะคงอยู่ได้นานเท่านาน ที่หน้าจอข่าวจะปรากฏขึ้น

หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับการทำงานกับ DataStore API โปรดดู DataStore

ไฟล์เป็นแหล่งข้อมูล

เมื่อทำงานกับออบเจ็กต์ขนาดใหญ่ เช่น ออบเจ็กต์ JSON หรือบิตแมป คุณจะต้องทำดังนี้ ทำงานร่วมกับออบเจ็กต์ File และจัดการการสลับชุดข้อความ

โปรดดูข้อมูลเพิ่มเติมเกี่ยวกับการใช้งานพื้นที่เก็บข้อมูลที่หัวข้อพื้นที่เก็บข้อมูล ภาพรวม

กำหนดเวลางานโดยใช้ WorkManager

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

WorkManager ช่วยให้ วางกำหนดเวลาการทำงานที่ไม่พร้อมกันและเชื่อถือได้ รวมถึงจัดการข้อจำกัดต่างๆ การจัดการ ไลบรารีที่แนะนำสำหรับงานถาวร หากต้องการดำเนินการ งานที่กำหนดไว้ข้างต้น Worker สร้างชั้นเรียนแล้ว: RefreshLatestNewsWorker ชั้นเรียนนี้ใช้เวลา NewsRepository เป็นทรัพยากร Dependency ในการดึงข้อมูลข่าวล่าสุดและแคชลงในดิสก์

class RefreshLatestNewsWorker(
    private val newsRepository: NewsRepository,
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result = try {
        newsRepository.refreshLatestNews()
        Result.success()
    } catch (error: Throwable) {
        Result.failure()
    }
}

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

ในตัวอย่างนี้ ต้องเรียกงานที่เกี่ยวข้องกับข่าวนี้จาก NewsRepository ซึ่งจะใช้แหล่งข้อมูลใหม่เป็นทรัพยากร Dependency: NewsTasksDataSource ดังต่อไปนี้

private const val REFRESH_RATE_HOURS = 4L
private const val FETCH_LATEST_NEWS_TASK = "FetchLatestNewsTask"
private const val TAG_FETCH_LATEST_NEWS = "FetchLatestNewsTaskTag"

class NewsTasksDataSource(
    private val workManager: WorkManager
) {
    fun fetchNewsPeriodically() {
        val fetchNewsRequest = PeriodicWorkRequestBuilder<RefreshLatestNewsWorker>(
            REFRESH_RATE_HOURS, TimeUnit.HOURS
        ).setConstraints(
            Constraints.Builder()
                .setRequiredNetworkType(NetworkType.TEMPORARILY_UNMETERED)
                .setRequiresCharging(true)
                .build()
        )
            .addTag(TAG_FETCH_LATEST_NEWS)

        workManager.enqueueUniquePeriodicWork(
            FETCH_LATEST_NEWS_TASK,
            ExistingPeriodicWorkPolicy.KEEP,
            fetchNewsRequest.build()
        )
    }

    fun cancelFetchingNewsPeriodically() {
        workManager.cancelAllWorkByTag(TAG_FETCH_LATEST_NEWS)
    }
}

ระบบจะตั้งชื่อชั้นเรียนประเภทนี้ตามข้อมูลที่องค์กรรับผิดชอบ ตัวอย่างเช่น NewsTasksDataSource หรือ PaymentsTasksDataSource งานทั้งหมดที่เกี่ยวข้อง ข้อมูลบางประเภทควรรวมอยู่ในคลาสเดียวกัน

หากต้องทริกเกอร์งานเมื่อเริ่มต้นแอป ขอแนะนำให้ทริกเกอร์ คำขอ WorkManager โดยใช้ App Startup ไลบรารีที่เรียกที่เก็บจาก Initializer

โปรดดูข้อมูลเพิ่มเติมเกี่ยวกับการทำงานกับ WorkManager API ที่ WorkManager

การทดสอบ

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

การทดสอบ 1 หน่วย

ใช้หลักเกณฑ์การทดสอบทั่วไปเมื่อทดสอบข้อมูล สำหรับการทดสอบ 1 หน่วย ให้ใช้ออบเจ็กต์จริงเมื่อจำเป็นและปลอมทรัพยากร Dependency ทั้งหมด ซึ่งเข้าถึงแหล่งข้อมูลภายนอก เช่น การอ่านจากไฟล์หรือการอ่านจาก เครือข่าย

การทดสอบการผสานรวม

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

สำหรับฐานข้อมูล Room ช่วยให้คุณสร้างฐานข้อมูลในหน่วยความจำ ควบคุมในการทดสอบ ดูข้อมูลเพิ่มเติมได้ที่หน้าทดสอบและแก้ไขข้อบกพร่อง ของฐานข้อมูล

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

ตัวอย่าง

ตัวอย่างของ Google ต่อไปนี้สาธิตการใช้ชั้นข้อมูล ศึกษาคู่มือเพื่อดูคำแนะนำนี้ในสถานการณ์จริง