แม้ว่าเลเยอร์ UI จะมีสถานะที่เกี่ยวข้องกับ UI และตรรกะ UI แต่ชั้นข้อมูล มีข้อมูลแอปพลิเคชันและตรรกะทางธุรกิจ ตรรกะทางธุรกิจคืออะไร ให้คุณค่าแก่แอป ซึ่งสร้างจากกฎเกณฑ์ทางธุรกิจในชีวิตจริงที่กำหนด วิธีสร้าง จัดเก็บ และเปลี่ยนแปลงข้อมูลแอปพลิเคชัน
การแยกข้อกังวลนี้ทำให้สามารถใช้ชั้นข้อมูลกับ แชร์หน้าจอ แชร์ข้อมูลระหว่างส่วนต่างๆ ของแอป และทำซ้ำ ตรรกะทางธุรกิจที่ไม่อยู่ใน UI สำหรับการทดสอบ 1 หน่วย สำหรับข้อมูลเพิ่มเติมเกี่ยวกับ ประโยชน์ของชั้นข้อมูล โปรดดูภาพรวมของสถาปัตยกรรม
สถาปัตยกรรมชั้นข้อมูล
ชั้นข้อมูลสร้างขึ้นจากที่เก็บซึ่งแต่ละที่เก็บอาจมีตั้งแต่ 0 ถึงหลายรายการ
แหล่งข้อมูล คุณควรสร้างคลาสที่เก็บสำหรับแต่ละประเภท
ข้อมูลที่คุณจัดการในแอป เช่น คุณอาจสร้างMoviesRepository
คลาสสำหรับข้อมูลที่เกี่ยวข้องกับภาพยนตร์ หรือคลาส PaymentsRepository
สำหรับข้อมูล
ที่เกี่ยวข้องกับการชำระเงิน
![ในสถาปัตยกรรมทั่วไป ที่เก็บของชั้นข้อมูลจะให้ข้อมูล
ส่วนที่เหลือของแอปและขึ้นอยู่กับแหล่งข้อมูล](https://developer.android.com/static/topic/libraries/architecture/images/mad-arch-data-overview.png?authuser=4&hl=th)
คลาสที่เก็บมีหน้าที่รับผิดชอบงานต่อไปนี้:
- กำลังเปิดเผยข้อมูลที่เหลือของแอป
- การรวมการเปลี่ยนแปลงไว้ในข้อมูลไว้ในที่เดียว
- การแก้ไขความขัดแย้งระหว่างแหล่งข้อมูลหลายแห่ง
- แหล่งที่มาของข้อมูลที่เป็นนามธรรมจากส่วนอื่นๆ ของแอป
- มีตรรกะทางธุรกิจ
คลาสแหล่งข้อมูลแต่ละคลาสควรมีหน้าที่รับผิดชอบในการทำงานด้วย แหล่งที่มาของข้อมูล ซึ่งอาจเป็นไฟล์ แหล่งที่มาของเครือข่าย หรือฐานข้อมูลในเครื่อง ข้อมูล คลาสแหล่งที่มาเป็นสะพานเชื่อมระหว่างแอปพลิเคชันและระบบสำหรับข้อมูล การดำเนินงาน
เลเยอร์อื่นๆ ในลำดับชั้นไม่ควรเข้าถึงแหล่งข้อมูลโดยตรง เวลา จุดแรกเข้าของชั้นข้อมูลจะเป็นคลาสที่เก็บเสมอ ผู้ถือรัฐ (ดูคู่มือเลเยอร์ 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 ซึ่งขึ้นอยู่กับแหล่งข้อมูลการลงทะเบียนอื่นๆ](https://developer.android.com/static/topic/libraries/architecture/images/mad-arch-data-multiple-repos.png?authuser=4&hl=th)
แหล่งที่มาของความจริง
ที่เก็บแต่ละแห่งจะต้องกำหนดแหล่งข้อมูลที่ถูกต้องเพียงแหล่งเดียว แหล่งที่มา ที่ถูกต้อง มีข้อมูลที่สอดคล้องกัน ถูกต้อง และเป็นปัจจุบันเสมอ ใน ข้อเท็จจริงที่เปิดเผยจากที่เก็บควรเป็นข้อมูลที่มาจาก จากแหล่งข้อมูลที่ถูกต้องโดยตรง
แหล่งข้อมูลที่ถูกต้องอาจเป็นแหล่งข้อมูล เช่น ฐานข้อมูล หรือแม้แต่ข้อมูล แคชในหน่วยความจำที่ที่เก็บอาจมีอยู่ รวมที่เก็บ แหล่งข้อมูลต่างๆ และแก้ไขข้อขัดแย้งที่อาจเกิดขึ้นระหว่างข้อมูล แหล่งที่ต้องอัปเดตแหล่งข้อมูลที่ถูกต้องแห่งเดียวเป็นประจำหรือเนื่องจากข้อมูลจากผู้ใช้ กิจกรรม
ที่เก็บที่ต่างกันในแอปอาจมีแหล่งที่มาที่ถูกต้องแตกต่างกัน สำหรับ
ตัวอย่างเช่น คลาส 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 ต่อไปนี้สาธิตการใช้ชั้นข้อมูล ศึกษาคู่มือเพื่อดูคำแนะนำนี้ในสถานการณ์จริง
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- เลเยอร์โดเมน
- สร้างแอปที่ใช้แบบออฟไลน์เป็นหลัก
- การใช้งานสถานะ UI