Panduan Arsitektur Aplikasi Android

Halo, Habr! Saya hadir untuk Anda terjemahan gratis "Panduan untuk arsitektur aplikasi" dari JetPack . Saya meminta Anda untuk meninggalkan semua komentar pada terjemahan di komentar, dan mereka akan diperbaiki. Juga, komentar dari mereka yang menggunakan arsitektur yang disajikan dengan rekomendasi untuk penggunaannya akan bermanfaat bagi semua orang.

Panduan ini mencakup praktik terbaik dan arsitektur yang disarankan untuk membangun aplikasi yang kuat. Halaman ini mengasumsikan pengantar dasar untuk Kerangka Android. Jika Anda baru dalam pengembangan aplikasi Android, lihat panduan pengembang kami untuk memulai dan mempelajari lebih lanjut tentang konsep yang disebutkan dalam panduan ini. Jika Anda tertarik pada arsitektur aplikasi dan ingin membiasakan diri dengan materi dalam panduan ini dalam hal pemrograman di Kotlin, lihat kursus Udacity, "Mengembangkan Aplikasi untuk Android dengan Kotlin," .

Pengalaman Pengguna Aplikasi Seluler


Dalam kebanyakan kasus, aplikasi desktop memiliki titik masuk tunggal dari desktop atau peluncur, dan kemudian dijalankan sebagai proses monolitik tunggal. Aplikasi Android memiliki struktur yang jauh lebih kompleks. Aplikasi Android tipikal berisi beberapa komponen aplikasi , termasuk Aktivitas , Fragmen , Layanan , ContentProviders, dan BroadcastReceivers .

Anda menyatakan semua atau beberapa komponen aplikasi ini dalam manifes aplikasi. Android kemudian menggunakan file ini untuk memutuskan bagaimana mengintegrasikan aplikasi Anda ke dalam antarmuka pengguna umum perangkat. Mengingat bahwa aplikasi Android yang ditulis dengan baik berisi beberapa komponen, dan pengguna sering berinteraksi dengan beberapa aplikasi dalam waktu singkat, aplikasi harus beradaptasi dengan berbagai jenis alur kerja dan tugas yang digerakkan pengguna.

Misalnya, pertimbangkan apa yang terjadi ketika Anda berbagi foto di aplikasi media sosial favorit Anda:

  1. Aplikasi memicu niat kamera. Android meluncurkan aplikasi kamera untuk memproses permintaan. Saat ini, pengguna telah meninggalkan aplikasi untuk jejaring sosial, dan pengalamannya sebagai pengguna sangat sempurna.
  2. Aplikasi kamera dapat memicu niat lain, seperti meluncurkan pemilih file, yang dapat meluncurkan aplikasi lain.
  3. Pada akhirnya, pengguna kembali ke aplikasi jejaring sosial dan berbagi foto.

Setiap saat dalam proses, pengguna dapat terganggu oleh panggilan telepon atau pemberitahuan. Setelah tindakan yang terkait dengan interupsi ini, pengguna berharap dapat kembali dan melanjutkan proses berbagi foto ini. Perilaku beralih aplikasi ini biasa terjadi pada perangkat seluler, jadi aplikasi Anda harus menangani poin (tugas) ini dengan benar.

Ingatlah bahwa perangkat seluler juga terbatas sumber dayanya, jadi kapan saja, sistem operasi dapat menghancurkan beberapa proses aplikasi untuk membebaskan ruang untuk yang baru.

Dengan kondisi lingkungan ini, komponen aplikasi Anda dapat diluncurkan secara terpisah dan tidak berurutan, dan sistem operasi atau pengguna dapat menghancurkannya kapan saja. Karena peristiwa ini tidak di bawah kendali Anda , Anda tidak boleh menyimpan data atau status apa pun dalam komponen aplikasi Anda, dan komponen aplikasi Anda tidak boleh saling bergantung.

Prinsip arsitektur umum


Jika Anda tidak boleh menggunakan komponen aplikasi untuk menyimpan data dan status aplikasi, bagaimana Anda harus mengembangkan aplikasi Anda?

Pembagian tanggung jawab


Prinsip yang paling penting untuk diikuti adalah pembagian tanggung jawab . Kesalahan umum adalah ketika Anda menulis semua kode Anda di Aktivitas atau Fragmen . Ini adalah kelas antarmuka pengguna yang hanya boleh berisi logika yang memproses interaksi antarmuka pengguna dan sistem operasi. Dengan berbagi tanggung jawab sebanyak mungkin dalam kelas-kelas ini (SRP) , Anda dapat menghindari banyak masalah yang terkait dengan siklus hidup aplikasi.

Kontrol Antarmuka Pengguna dari Model


Prinsip penting lainnya adalah Anda harus mengontrol antarmuka pengguna Anda dari suatu model , lebih disukai dari model permanen. Model adalah komponen yang bertanggung jawab untuk memproses data untuk aplikasi. Mereka tidak tergantung pada objek Lihat dan komponen aplikasi, oleh karena itu, mereka tidak terpengaruh oleh siklus hidup aplikasi dan masalah terkait.

Model permanen sangat ideal karena alasan berikut:

  • Pengguna Anda tidak akan kehilangan data jika OS Android menghancurkan aplikasi Anda untuk membebaskan sumber daya.
  • Aplikasi Anda terus berfungsi ketika koneksi jaringan tidak stabil atau tidak tersedia.

Dengan mengorganisasi fondasi aplikasi Anda ke dalam kelas model dengan tanggung jawab yang jelas untuk manajemen data, aplikasi Anda menjadi lebih teruji dan didukung.

Arsitektur Aplikasi yang Disarankan


Bagian ini menunjukkan bagaimana menyusun aplikasi menggunakan komponen arsitektur , bekerja dalam skenario penggunaan ujung ke ujung .

Catatan Tidaklah mungkin untuk memiliki satu cara menulis aplikasi yang bekerja paling baik untuk setiap skenario. Namun, arsitektur yang direkomendasikan adalah titik awal yang baik untuk sebagian besar situasi dan alur kerja. Jika Anda sudah memiliki cara yang baik untuk menulis aplikasi Android yang memenuhi prinsip arsitektur umum, Anda tidak boleh mengubahnya.

Bayangkan kami membuat antarmuka pengguna yang menampilkan profil pengguna. Kami menggunakan API pribadi dan API REST untuk mengambil data profil.

Ulasan


Untuk memulai, pertimbangkan skema interaksi modul arsitektur aplikasi yang sudah selesai:



Harap dicatat bahwa setiap komponen hanya bergantung pada komponen satu tingkat di bawahnya. Misalnya, Aktivitas dan Fragmen hanya bergantung pada model tampilan. Repositori adalah satu-satunya kelas yang bergantung pada banyak kelas lainnya; dalam contoh ini, penyimpanan tergantung pada model data yang persisten dan sumber data internal jarak jauh.

Pola desain ini menciptakan pengalaman pengguna yang konsisten dan menyenangkan. Terlepas dari apakah pengguna kembali ke aplikasi beberapa menit setelah menutupnya atau beberapa hari kemudian, ia akan langsung melihat informasi pengguna bahwa aplikasi tersebut disimpan secara lokal. Jika data ini kedaluwarsa, modul penyimpanan aplikasi mulai memperbarui data di latar belakang.

Buat antarmuka pengguna


Antarmuka pengguna terdiri dari fragmen UserProfileFragment dan user_profile_layout.xml layout user_profile_layout.xml sesuai.

Untuk mengelola antarmuka pengguna, model data kami harus berisi elemen data berikut:

  • ID Pengguna: ID pengguna. Solusi terbaik adalah meneruskan informasi ini ke fragmen menggunakan argumen fragmen. Jika OS Android menghancurkan proses kami, informasi ini disimpan, sehingga pengidentifikasi akan tersedia saat berikutnya kami meluncurkan aplikasi kami.
  • Objek pengguna: kelas data yang berisi informasi pengguna.

Kami menggunakan UserProfileViewModel berdasarkan pada komponen arsitektur ViewModel untuk menyimpan informasi ini.

Objek ViewModel menyediakan data untuk komponen antarmuka pengguna tertentu, seperti fragmen atau Kegiatan, dan berisi logika pemrosesan data bisnis untuk berinteraksi dengan model. Misalnya, ViewModel dapat memanggil komponen lain untuk memuat data dan dapat meneruskan permintaan pengguna untuk perubahan data. ViewModel tidak tahu tentang komponen-komponen antarmuka pengguna, sehingga tidak terpengaruh oleh perubahan konfigurasi, seperti menciptakan kembali Aktivitas saat perangkat diputar.

Sekarang kami telah mengidentifikasi file-file berikut:

  • user_profile.xml : tata letak antarmuka pengguna yang ditentukan.
  • UserProfileFragment : menggambarkan pengontrol antarmuka pengguna yang bertanggung jawab untuk menampilkan informasi kepada pengguna.
  • UserProfileViewModel : kelas yang bertanggung jawab untuk menyiapkan data untuk ditampilkan di UserProfileFragment dan menanggapi interaksi pengguna.

Cuplikan kode berikut menunjukkan konten awal dari file-file ini. (File tata letak dihilangkan karena kesederhanaan.)

 class UserProfileViewModel : ViewModel() { val userId : String = TODO() val user : User = TODO() } class UserProfileFragment : Fragment() { private val viewModel: UserProfileViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return inflater.inflate(R.layout.main_fragment, container, false) } } 

Sekarang kita memiliki modul kode ini, bagaimana kita menghubungkannya? Setelah bidang pengguna diatur dalam kelas UserProfileViewModel, kita perlu cara untuk memberi tahu antarmuka pengguna.

Catatan SavedStateHandle memungkinkan ViewModel untuk mengakses keadaan tersimpan dan argumen dari fragmen atau tindakan yang terkait.

 // UserProfileViewModel class UserProfileViewModel( savedStateHandle: SavedStateHandle ) : ViewModel() { val userId : String = savedStateHandle["uid"] ?: throw IllegalArgumentException("missing user id") val user : User = TODO() } // UserProfileFragment private val viewModel: UserProfileViewModel by viewModels( factoryProducer = { SavedStateVMFactory(this) } ... ) 

Sekarang kita perlu memberi tahu Fragmen kita ketika objek pengguna diterima. Di sinilah komponen arsitektur LiveData muncul.

LiveData adalah pemegang data yang dapat diamati. Komponen lain dalam aplikasi Anda dapat melacak perubahan pada objek menggunakan dudukan ini, tanpa membuat jalur ketergantungan yang eksplisit dan sulit di antara mereka. Komponen LiveData juga memperhitungkan keadaan siklus hidup komponen aplikasi Anda, seperti Aktivitas, Fragmen, dan Layanan, dan termasuk logika pembersihan untuk mencegah kebocoran objek dan konsumsi memori yang berlebihan.

Catatan Jika Anda sudah menggunakan pustaka seperti RxJava atau Agera, Anda bisa terus menggunakannya daripada LiveData. Namun, saat menggunakan perpustakaan dan pendekatan serupa, pastikan Anda menangani siklus hidup aplikasi Anda dengan benar. Secara khusus, pastikan bahwa Anda menangguhkan aliran data Anda ketika LifecycleOwner terkait dihentikan, dan hancurkan aliran ini ketika LifecycleOwner terkait telah dihancurkan. Anda juga dapat menambahkan artifact android.arch.lifecycle: jet stream untuk menggunakan LiveData dengan pustaka aliran jet lain seperti RxJava2.

Untuk memasukkan komponen LiveData dalam aplikasi kami, kami mengubah jenis bidang di UserProfileViewModel menjadi LiveData. UserProfileFragment sekarang diinformasikan tentang pembaruan data. Selain itu, karena bidang LiveData ini mendukung siklus masa pakai, bidang ini secara otomatis menghapus tautan saat tidak diperlukan lagi.

 class UserProfileViewModel( savedStateHandle: SavedStateHandle ) : ViewModel() { val userId : String = savedStateHandle["uid"] ?: throw IllegalArgumentException("missing user id") val user : LiveData<User> = TODO() } 

Sekarang kami memodifikasi UserProfileFragment untuk mengamati data dalam ViewModel dan memperbarui antarmuka pengguna sesuai dengan perubahan:

 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.user.observe(viewLifecycleOwner) { //  UI } } 

Setiap kali data profil pengguna diperbarui, panggilan balik onChanged () dipanggil dan antarmuka pengguna diperbarui.

Jika Anda terbiasa dengan perpustakaan lain yang menggunakan panggilan balik yang dapat diobservasi, Anda mungkin telah menyadari bahwa kami tidak mendefinisikan kembali metode onStop () dari fragmen untuk berhenti mengamati data. Langkah ini opsional untuk LiveData karena mendukung siklus hidup, yang berarti tidak akan memanggil panggilan balik onChanged() jika fragmennya dalam keadaan tidak aktif; yaitu, ia menerima panggilan ke onStart () , tetapi belum menerima onStop() ). LiveData juga secara otomatis menghapus pengamat saat memanggil metode onDestroy () pada fragmen.

Kami belum menambahkan logika apa pun untuk menangani perubahan konfigurasi, seperti memutar layar perangkat oleh pengguna. UserProfileViewModel secara otomatis dikembalikan ketika konfigurasi diubah, sehingga segera setelah sebuah fragmen baru dibuat, ia mendapat instance ViewModel sama, dan panggilan balik dipanggil segera menggunakan data saat ini. Mengingat bahwa objek ViewModel dirancang untuk bertahan dari objek View terkait yang mereka perbarui, Anda tidak boleh menyertakan referensi langsung ke objek View dalam implementasi ViewModel Anda. Untuk informasi lebih lanjut tentang masa pakai ViewModel sesuai dengan siklus hidup komponen antarmuka pengguna, lihat Siklus hidup ViewModel.

Pengambilan data


Sekarang kita telah menggunakan LiveData untuk menghubungkan UserProfileViewModel ke UserProfileFragment , bagaimana kita bisa mendapatkan data profil pengguna?

Dalam contoh ini, kami menganggap bahwa backend kami menyediakan API REST. Kami menggunakan pustaka Retrofit untuk mengakses backend kami, meskipun Anda dapat menggunakan pustaka yang berbeda yang memiliki tujuan yang sama.

Berikut adalah definisi kami tentang layanan Web yang menautkan ke backend kami:

 interface Webservice { /** * @GET declares an HTTP GET request * @Path("user") annotation on the userId parameter marks it as a * replacement for the {user} placeholder in the @GET path */ @GET("/users/{user}") fun getUser(@Path("user") userId: String): Call<User> } 

Ide pertama untuk mengimplementasikan ViewModel mungkin melibatkan memanggil layanan Web Webservice untuk mengambil data dan menetapkan data itu ke objek LiveData kami. Desain ini berfungsi, tetapi menggunakannya membuat aplikasi kita lebih sulit untuk dipertahankan seiring pertumbuhannya. Ini memberikan terlalu banyak tanggung jawab kepada kelas UserProfileViewModel , yang melanggar prinsip pemisahan kepentingan . Selain itu, ruang lingkup ViewModel dikaitkan dengan siklus hidup Aktivitas atau Fragmen , yang berarti bahwa data dari layanan Web hilang ketika siklus hidup objek antarmuka pengguna terkait berakhir. Perilaku ini menciptakan pengalaman pengguna yang tidak diinginkan.

Alih-alih, ViewModel kami mendelegasikan proses pengambilan data ke modul penyimpanan baru.

Modul repositori menangani operasi data. Mereka menyediakan API bersih sehingga sisa aplikasi dapat dengan mudah mendapatkan data ini. Mereka tahu di mana mendapatkan data dan panggilan API apa yang harus dilakukan saat memperbarui data. Anda dapat menganggap repositori sebagai perantara antara berbagai sumber data, seperti model persisten, layanan web, dan cache.

Kelas UserRepository kami, ditampilkan dalam cuplikan kode berikut, menggunakan instance WebService untuk mengambil data pengguna:

 class UserRepository { private val webservice: Webservice = TODO() // ... fun getUser(userId: String): LiveData<User> { //    .    . val data = MutableLiveData<User>() webservice.getUser(userId).enqueue(object : Callback<User> { override fun onResponse(call: Call<User>, response: Response<User>) { data.value = response.body() } //     . override fun onFailure(call: Call<User>, t: Throwable) { TODO() } }) return data } } 

Meskipun modul penyimpanan tampaknya tidak perlu, itu melayani tujuan penting: itu abstrak sumber data dari sisa aplikasi. Sekarang UserProfileViewModel kami tidak tahu cara mengambil data, sehingga kami dapat menyediakan model presentasi dengan data yang diperoleh dari beberapa implementasi ekstraksi data yang berbeda.

Catatan Kami melewatkan kasus kesalahan jaringan karena kesederhanaan. Untuk implementasi alternatif yang mengekspos kesalahan dan status unduhan, lihat Lampiran: Pengungkapan Status Jaringan.

Mengelola Ketergantungan antar Komponen

Kelas UserRepository atas membutuhkan turunan dari layanan Web untuk mengambil data pengguna. Dia hanya bisa membuat contoh, tetapi untuk ini dia juga perlu tahu dependensi kelas layanan Web. Selain itu, UserRepository mungkin bukan satu-satunya kelas yang membutuhkan layanan web. Situasi ini mengharuskan kami untuk menduplikasi kode, karena setiap kelas yang membutuhkan tautan ke layanan Web perlu tahu cara membuatnya dan ketergantungannya. Jika setiap kelas membuat WebService baru, aplikasi kita bisa menjadi sangat padat sumber daya.

Untuk mengatasi masalah ini, Anda dapat menggunakan pola desain berikut:

  • Ketergantungan Injeksi (DI) . Injeksi ketergantungan memungkinkan kelas untuk menentukan dependensi mereka tanpa membuatnya. Pada saat run time, kelas lain bertanggung jawab untuk menyediakan dependensi ini. Kami merekomendasikan perpustakaan Dagger 2 untuk menerapkan injeksi ketergantungan pada aplikasi Android. Belati 2 secara otomatis membuat objek, melewati pohon dependensi, dan memberikan jaminan waktu kompilasi untuk dependensi.
  • (Lokasi layanan) Lokasi layanan : Templat lokasi layanan menyediakan registri di mana kelas bisa mendapatkan dependensi mereka alih-alih membangunnya.

Menerapkan registri layanan lebih mudah daripada menggunakan DI, jadi jika Anda baru mengenal DI, gunakan templat: lokasi layanan sebagai gantinya.

Templat ini memungkinkan Anda untuk menskalakan kode Anda karena mereka memberikan templat yang jelas untuk mengelola dependensi tanpa menduplikasi atau menyulitkan kode. Selain itu, template ini memungkinkan Anda untuk dengan cepat beralih antara pengujian dan implementasi pengambilan sampel data.

Aplikasi sampel kami menggunakan Belati 2 untuk mengelola dependensi objek layanan Web.

Hubungkan ViewModel dan Penyimpanan


Sekarang kita memodifikasi UserProfileViewModel kita untuk menggunakan objek UserRepository :

 class UserProfileViewModel @Inject constructor( savedStateHandle: SavedStateHandle, userRepository: UserRepository ) : ViewModel() { val userId : String = savedStateHandle["uid"] ?: throw IllegalArgumentException("missing user id") val user : LiveData<User> = userRepository.getUser(userId) } 

Caching


Implementasi UserRepository mengabstraksi permohonan objek layanan Web, tetapi karena hanya bergantung pada satu sumber data, itu tidak terlalu fleksibel.

Masalah utama dengan implementasi UserRepository adalah bahwa setelah menerima data dari backend kami, data ini tidak disimpan di mana pun. Oleh karena itu, jika pengguna meninggalkan UserProfileFragment dan kemudian kembali ke sana, aplikasi kita harus mengambil data, bahkan jika mereka tidak berubah.

Desain ini tidak optimal karena alasan berikut:

  • Ini menghabiskan sumber daya lalu lintas yang berharga.
  • Hal ini membuat pengguna menunggu penyelesaian permintaan baru.

Untuk mengatasi kekurangan ini, kami menambahkan sumber data baru ke UserRepository kami, yang menyimpan benda-benda User dalam memori:

 // Dagger,        . @Singleton class UserRepository @Inject constructor( private val webservice: Webservice, //    .    . private val userCache: UserCache ) { fun getUser(userId: String): LiveData<User> { val cached = userCache.get(userId) if (cached != null) { return cached } val data = MutableLiveData<User>() userCache.put(userId, data) //     ,  ,  . //      . webservice.getUser(userId).enqueue(object : Callback<User> { override fun onResponse(call: Call<User>, response: Response<User>) { data.value = response.body() } //     . override fun onFailure(call: Call<User>, t: Throwable) { TODO() } }) return data } } 

Data persisten


Menggunakan implementasi kami saat ini, jika pengguna memutar perangkat atau pergi dan segera kembali ke aplikasi, antarmuka pengguna yang ada menjadi segera terlihat, karena toko mengambil data dari cache kami di memori.

Namun, apa yang terjadi jika pengguna meninggalkan aplikasi dan kembali beberapa jam setelah OS Android menyelesaikan prosesnya? Mengandalkan implementasi kami saat ini dalam situasi ini, kami perlu mendapatkan data dari jaringan lagi. Proses peningkatan ini bukan hanya pengalaman pengguna yang buruk; itu juga boros karena mengkonsumsi data seluler yang berharga.

Anda dapat memecahkan masalah ini dengan melakukan caching permintaan web, tetapi ini menciptakan masalah baru yang utama: apa yang terjadi jika data pengguna yang sama ditampilkan dalam permintaan dari jenis yang berbeda, misalnya, saat menerima daftar teman? Aplikasi akan menampilkan data yang saling bertentangan, yang paling membingungkan. Misalnya, aplikasi kami dapat menampilkan dua versi data yang berbeda dari pengguna yang sama jika pengguna mengirim permintaan daftar teman dan permintaan satu pengguna pada waktu yang berbeda. Aplikasi kita harus mencari cara untuk menggabungkan data yang bertentangan ini.

Cara yang tepat untuk menghadapi situasi ini adalah dengan menggunakan model konstan. Room Permanent Data Library (DB) siap membantu kami.

Room adalah perpustakaan pemetaan objek yang menyediakan penyimpanan data lokal dengan kode standar minimum. Pada waktu kompilasi, ia memeriksa setiap permintaan untuk kepatuhan dengan skema data Anda, sehingga permintaan SQL yang rusak menghasilkan kesalahan selama kompilasi, dan tidak crash saat runtime. Ruang abstrak dari beberapa detail implementasi dasar dari tabel dan kueri SQL mentah. Ini juga memungkinkan Anda untuk mengamati perubahan dalam data database, termasuk koleksi dan permintaan koneksi, mengekspos perubahan tersebut menggunakan objek LiveData. Itu bahkan secara eksplisit mendefinisikan kendala eksekusi yang memecahkan masalah threading umum, seperti akses ke penyimpanan di utas utama.

Catatan Jika aplikasi Anda sudah menggunakan solusi lain, seperti SQLite Object Relational Mapping (ORM), Anda tidak perlu mengganti solusi yang ada dengan Room. Namun, jika Anda menulis aplikasi baru atau mengatur ulang aplikasi yang sudah ada, sebaiknya gunakan Kamar untuk menyimpan data aplikasi Anda. Dengan demikian, Anda dapat memanfaatkan abstraksi perpustakaan dan verifikasi kueri.

Untuk menggunakan Kamar, kita perlu mendefinisikan tata letak lokal kita. Pertama, kami menambahkan penjelasan @Entity ke kelas model data User kami dan penjelasan @PrimaryKey di bidang id kelas. Anotasi ini menandai User sebagai tabel dalam basis data kami, dan id sebagai kunci utama tabel:

 @Entity data class User( @PrimaryKey private val id: String, private val name: String, private val lastName: String ) 

Kemudian kami membuat kelas database dengan mengimplementasikan RoomDatabase untuk aplikasi kami:

 @Database(entities = [User::class], version = 1) abstract class UserDatabase : RoomDatabase() 

Perhatikan bahwa UserDatabase abstrak. Pustaka Room secara otomatis menyediakan implementasi ini. Lihat dokumentasi untuk Kamar untuk detailnya.

Sekarang kita membutuhkan cara untuk memasukkan data pengguna ke dalam basis data. Untuk tugas ini, kami membuat objek akses data (DAO) .

 @Dao interface UserDao { @Insert(onConflict = REPLACE) fun save(user: User) @Query("SELECT * FROM user WHERE id = :userId") fun load(userId: String): LiveData<User> } 

Perhatikan bahwa metode load mengembalikan objek bertipe LiveData. Kamar tahu kapan database diubah, dan secara otomatis memberi tahu semua pengamat aktif tentang perubahan data. Karena Room menggunakan LiveData , operasi ini efisien; itu memperbarui data hanya jika ada setidaknya satu pengamat aktif.

Catatan: Pemeriksaan kamar untuk pembatalan berdasarkan modifikasi tabel, yang berarti dapat mengirim pemberitahuan positif palsu.

Setelah mendefinisikan kelas UserDao kami, kami kemudian merujuk DAO dari kelas basis data kami:

 @Database(entities = [User::class], version = 1) abstract class UserDatabase : RoomDatabase() { abstract fun userDao(): UserDao } 

Sekarang kita dapat mengubah UserRepository kita untuk memasukkan sumber data Room:

 //  Dagger,         . @Singleton class UserRepository @Inject constructor( private val webservice: Webservice, //    .    . private val executor: Executor, private val userDao: UserDao ) { fun getUser(userId: String): LiveData<User> { refreshUser(userId) //   LiveData    . return userDao.load(userId) } private fun refreshUser(userId: String) { //    . executor.execute { // ,      . val userExists = userDao.hasUser(FRESH_TIMEOUT) if (!userExists) { //  . val response = webservice.getUser(userId).execute() //    . //   .  LiveData  , //        . userDao.save(response.body()!!) } } } companion object { val FRESH_TIMEOUT = TimeUnit.DAYS.toMillis(1) } } 

Harap perhatikan bahwa meskipun kami mengubah sumber data dalam UserRepository , kami tidak perlu mengubah UserProfileViewModel atau UserProfileFragment . Pembaruan kecil ini menunjukkan fleksibilitas yang disediakan arsitektur aplikasi kami. Ini juga bagus untuk pengujian karena kami dapat menyediakan UserRepository palsu dan menguji UserProfileViewModel produksi kami secara bersamaan.

Jika pengguna kembali dalam beberapa hari, maka aplikasi yang menggunakan arsitektur ini kemungkinan akan menampilkan informasi yang sudah ketinggalan zaman sampai repositori menerima informasi yang diperbarui. Tergantung pada kasus penggunaan Anda, Anda mungkin tidak menampilkan informasi yang ketinggalan zaman. Alih-alih, Anda bisa menampilkan data placeholder , yang menunjukkan nilai dummy dan menunjukkan bahwa aplikasi Anda sedang memuat dan memuat informasi terbaru.



REST API . , , , API, , . UserRepository Webservice , , , .

UserRepository - . LiveData. Menggunakan model ini, database berfungsi sebagai satu-satunya sumber kebenaran , dan bagian lain dari aplikasi mengaksesnya melalui kita UserRepository. Terlepas dari apakah Anda menggunakan cache disk, kami menyarankan repositori Anda mengidentifikasi sumber data sebagai satu-satunya sumber kebenaran untuk sisa aplikasi Anda.

Tampilkan kemajuan operasi


, pull-to-refresh, , , . , . , , , LiveData. , β€” , User .

, :



, .

, :

  • : Android UI . β€” Espresso . UserProfileViewModel . UserProfileViewModel , () .
  • ViewModel: UserProfileViewModel JUnit . , UserRepository .
  • UserRepository: UserRepository JUnit. Webservice UserDao . :

    • -.
    • .
    • , .

  • Webservice , UserDao , .
  • UserDao: DAO . - , . , , , …

    : Room , DAO, JSQL SupportSQLiteOpenHelper . , SQLite SQLite .
  • -: . , -, . , MockWebServer , .
  • : maven . androidx.arch.core : JUnit:

    • InstantTaskExecutorRule: .
    • CountingTaskExecutorRule: . Espresso .



β€” , Android . , , , , .

, , , :

β€” , β€” .

, , . , .

.

, , , . β€” β€” .

.

Tahan godaan untuk membuat label β€œhanya satu” yang mengungkapkan detail implementasi internal dari satu modul. Anda mungkin mendapatkan waktu dalam jangka pendek, tetapi kemudian Anda akan mengalami hutang teknis beberapa kali seiring basis kode Anda berkembang.

Pikirkan tentang bagaimana membuat setiap modul dapat diuji secara terpisah.

Misalnya, memiliki API yang terdefinisi dengan baik untuk mengambil data dari jaringan membuatnya mudah untuk menguji modul yang menyimpan data ini dalam database lokal. Jika sebaliknya Anda mencampur logika kedua modul ini di satu tempat atau mendistribusikan kode jaringan Anda di seluruh basis kode, pengujian menjadi jauh lebih sulit - dalam beberapa kasus bahkan tidak mustahil.

Fokus pada inti unik aplikasi Anda untuk menonjol dari aplikasi lain.

, . , , Android .

.

, , . , .

.

, , .

Tambahan: pengungkapan status jaringan


Pada bagian di atas arsitektur aplikasi yang direkomendasikan, kami melewatkan kesalahan jaringan dan status boot untuk menyederhanakan cuplikan kode.

Bagian ini menunjukkan cara menampilkan status jaringan menggunakan kelas Sumber Daya, yang merangkum data dan kondisinya.

Cuplikan kode berikut memberikan contoh implementasiResource:

 //  ,         . sealed class Resource<T>( val data: T? = null, val message: String? = null ) { class Success<T>(data: T) : Resource<T>(data) class Loading<T>(data: T? = null) : Resource<T>(data) class Error<T>(message: String, data: T? = null) : Resource<T>(data, message) } 

, , . NetworkBoundResource .

NetworkBoundResource :



. , NetworkBoundResource , , , . , , , , , .

, . NetworkBoundResource .

. . , .

, , , , , .

, , , . , , , . `SUCCESS` , .

API, NetworkBoundResource :

 // ResultType:   . // RequestType:   API. abstract class NetworkBoundResource<ResultType, RequestType> { //      API   . @WorkerThread protected abstract fun saveCallResult(item: RequestType) //      ,  ,    //     . @MainThread protected abstract fun shouldFetch(data: ResultType?): Boolean //        . @MainThread protected abstract fun loadFromDb(): LiveData<ResultType> //     API. @MainThread protected abstract fun createCall(): LiveData<ApiResponse<RequestType>> // ,    .   //    ,    . protected open fun onFetchFailed() {} //   LiveData,  , //    . fun asLiveData(): LiveData<ResultType> = TODO() } 

:

  • , ResultType RequestType , , API, , .
  • Ini menggunakan kelas ApiResponseuntuk permintaan jaringan. ApiResponseMerupakan pembungkus sederhana untuk kelas Retrofit2.Callyang mengubah respons menjadi instance LiveData.

Implementasi penuh kelas NetworkBoundResourcemuncul sebagai bagian dari proyek android-Architecture-komponen GitHub .

Setelah dibuat, NetworkBoundResourcekita dapat menggunakannya untuk menulis disk dan implementasi yang terlampir Userpada jaringan di kelas UserRepository:

 //  Dagger2,         . @Singleton class UserRepository @Inject constructor( private val webservice: Webservice, private val userDao: UserDao ) { fun getUser(userId: String): LiveData<User> { return object : NetworkBoundResource<User, User>() { override fun saveCallResult(item: User) { userDao.save(item) } override fun shouldFetch(data: User?): Boolean { return rateLimiter.canFetch(userId) && (data == null || !isFresh(data)) } override fun loadFromDb(): LiveData<User> { return userDao.load(userId) } override fun createCall(): LiveData<ApiResponse<User>> { return webservice.getUser(userId) } }.asLiveData() } } 

Source: https://habr.com/ru/post/id456256/


All Articles