Android MVVM — Part 2 Repository Sumber Data Kita
Membangun Aplikasi MVVM Dengan Kotlin, Coroutines dan Architecture Component
Repository
Sebelumnya kita udah bahas tentang pola MVVM waktu bikin aplikasi Android. Seperti yang udah kita bahas, View akan melakukan observasi terhadap data di ViewModel dan memiliki tanggung jawab untuk update tampilan ketika ada perubahan data di ViewModel. Artinya View memiliki ketergantungan terhadap ViewModel. Sedangkan ViewModel akan menyimpan data dalam bentuk LiveData yang nantinya akan diobservasi oleh View. Dari mana asal data tersebut? Yaps benar! Repository.
Repository bertanggung jawab untuk semua data yang akan digunakan di aplikasi. Mau itu menyimpan data, melakukan update data, menghapus data atau mencari data serahkan semuanya kepada Repository. Kemana Repository akan menyimpan data atau dari mana Repository akan mencari data? Biasanya aplikasi Android melakukan penyimpanan ke dua tempat. Pertama Remote Data Store atau web service yang dimiliki. Remote Data Store biasanya berupa cloud server atau server fisik. Yang kedua adalah Local Data Store. Kebalikan dari Remote Data Store, Local Data Store menyimpan datanya langsung di device Android itu sendiri baik menggunakan Realm atau SQLite.
Kenapa Dua Data Store?
Banyak alasan kenapa banyak aplikasi masih menyimpan ke Local Data Store, walaupun sebenarnya sudah tersimpan di Remote Data Store. Kalau diliat dari tujuan-pun kedua Data Store tersebut udah jelas beda.
Salah satu tujuan dari Remote Data Store itu buat sinkronisasi data antar device. Jadi kalau misal ada pengguna yang ganti device, dia cukup login pake akun yang sama dengan device sebelumnya. Jadi data dari device sebelumnya masih bisa dipake di device yang baru.
Kalau Local Data Store, biasanya dipake buat ningkatin pengalaman pengguna saat pake aplikasi. Mencari data pada SQLite local jauh lebih cepat dibanding melakukan request http ke server. Belum lagi device harus terhubung ke internet.
Dengan memanfaatkan keduanya, kita bisa membuat aplikasi kita menjadi Offline First Apps.
Yuk Bikin!
Halaman pertama dari aplikasi yang kita bangun akan menampilkan list dari pokemon set yang tersedia. Tampilannya kurang lebih akan seperti ini.
Sebelum kita membangun repository, kita akan membangun model dari set pokemon ini.
Kita akan membangun repository untuk set pokemon ini.
Kayak yang udah dijelasin sebelumnya, Repository akan bergantung pada dua buah Data Store. Untuk mendapatkan set pokemon yang tersedia, pertama repository bakal cari data dari Local Data Store dulu. Kalau datanya ketemu berarti datanya langsung dikirim. Kalau nggak ketemu, Repository bakal cari data dari Remote Data Store. Abis dapet datanya, Repository bakal nyimpen datanya ke Local Data Store.
PokemonSetDataStore adalah sebuah interface yang didefinisikan sebagai berikut.
Setelah mendefinisikan interface tersebut, kita harus mendefinisikan implementasinya. Biar bisa pake fitur coroutine dari kotlin, kita bikin fungsinya jadi suspend function. Karena Repository kita membutuhkan 2 buah Data Store, maka kita akan membuat 2 class DataStore baru.
Pertama kita buat PokemonSetLocalDataStore. Data Store ini akan menyimpan data dari web service di variabel cache. Gimana kalo misalnya kita bikin object Repository baru dan DataStore baru, berarti variabel cache nanti bakal kosong padahal kita udah nyimpen datanya? Yaps benar, data cache yang seharusnya udah disimpan sebelumnya berubah lagi menjadi list baru yang masih kosong.
Solusi dari skenario itu adalah dengan memastikan object repository yang kita bikin cuma ada satu dan kita hanya melakukan set up objectnya hanya sekali. Untuk memastikan object Repository yang kita bangun cuma ada satu adalah dengan mengimplementasikan Singleton Pattern.
Selanjutnya kita buat class PokemonSetRemoteDataStore. Tugas dari class ini adalah melakukan request http menggunakan library Retrofit 2. Sebenarnya penamaan class ini bisa saja menjadi PokemonSetRetrofitDataStore. Bergantung selera dan code convention yang digunakan dalam tim.
Kalau nanti kita butuh sebuah Data Store baru yang dapat menyimpan data ke dalam SQLite pake library Room. Kita nggak perlu ngubah class PokemonSetLocalDataStore, kita cukup buat sebuah class baru misalnya PokemonSetRoomDataStore dan mengimplementasikan fungsi yang dibutuhkan.
Untuk service Retrofit yang digunakan, implementasinya sebagai berikut.
Buat class test untuk nguji repository yang kita bangun.
Test yang kita buat bakal nguji apakah repository yang kita bangun bekerja sesuai dengan konsep yang sudah dijelaskan atau belum.
Fungsi shouldNotGetPokemonsFromRemoteWhenLocalIsNotNull buat nguji apa Repository bakal melakukan request http kalau data dari Local Data Store nggak null atau malah langsung kirim data dari Local Data Store. Yang kita pengen adalah repository langsung ngirim data dari Local Data Store dan nggak melakukan request http sama sekali.
Fungsi kedua shouldCallGetPokemonsFromRemoteAndSaveToLocalWhenLocalIsNull buat nguji apa Repository bakal melakukan request http kalau data dari Local Data Store null, menyimpan data dari hasil request http ke Local Data Store dan kirim hasil datanya.
Fungsi yang terakhir buat mastiin kalau repository akan melempar exception ketika terjadi kegagalan waktu melakukan request http.
Repository yang kita bangun sudah sesuai dengan yang kita pengen. Selanjutnya kita tinggal bangun Repository buat mendapatkan kartu pokemon.
Kayak yang udah dilakuin sebelumnya, pertama kita definisiin dulu model buat kartu pokemon.
Selanjutnya kita bakal bangun repository dari kartu pokemon dan memastikan repository mengimplementasikan Singleton Pattern.
Ada yang ganjel? Berasa dejavu? Kita coba bandingin class PokemonCardRepository dengan PokemonSetReporitory.
Ternyata bener, ada sesuatu yang ngulang dan sama persis di PokemonCardRepository sama PokemonSetRepository. Kita bakal menggeneralisasi dua class ini jadi nggak akan dejavu lagi.
Untuk mastiin perubahan ini nggak bikin repository sebelumnya error, kita jalanin lagi unit test yang udah dibuat.
Mantap, semuanya lancar. Oke berikutnya kita harus definisikan PokemonCardDataStore biar PokemonCardRepository nggak error.
Dejavu lagi kah? PokemonCardDataStore dan PokemonSetDataStore punya banyak kesamaan. Mereka cuma punya 2 fungsi, yang pertama buat mendapatkan list dan yang kedua untuk menyimpan list yang sudah didapatkan. Apa perlu digeneralisasi lagi? Hmmm kayaknya nggak. Kenapa? Karena selain mereka butuh parameter yang beda, mereka juga punya tanggung jawab yang beda.
PokemonCardDataStore bertanggung jawab buat kartu pokemon sedangkan PokemonSetDataStore bertanggung jawab buat set pokemon. Kalau kita liat lagi ke sebelumnya di PokemonSetRemoteDataStore, fungsi addAll tidak diimplementasikan sama sekali. Jadi kalau misal kita generalisasi 2 interface tersebut menjadi 1 interface, nanti kalau ada perubahan proses bisnis di kartu pokemon di mana kita bisa tambah kartu favorit berarti di interface yang baru kita bakal nambah 1 fungsi addFavorite. Kalau gitu PokemonSetLocalDataStore dan PokemonSetRemoteDataStore harus mengimplementasikan fungsi addToFavorite tersebut padahal ga ada implementasi yang jelas buat set pokemon.
Overriding fungsi yang tidak diperlukan bukan best practice. Jadi hindari code yang kayak gitu ya.
“A class should have only one reason to change” Robert C. Martin
Kita balik ke PokemonCardDataStore, sekarang kita implementasikan menjadi PokemonLocalDataStore dan PokemonRemoteDataStore.
Ada satu perbedaan dari PokemonCardLocalDataStore dan PokemonSetLocalDataStore. Kalau sebelumnya kita simpan cache di MutableList, sekarang kita simpan di MutableMap. Karena kartu pokemon yang ditampilkan bakal beda-beda tergantung dari set yang diklik, makannya cache-nya disimpan dalam bentuk MutableMap. Kita bakal cari kartu pokemon sesuai dengan set yang diklik, kalau misalnya ga ada kita bakal request http buat dapet data sesuai set tersebut.
Terakhir kita harus test repository yang udah kita dibangun.
Dan seperti sebelumnya, fungsi pertama untuk menguji kalau repository akan melakukan request http atau nggak kalau data dari Local Data Store tidak null. Fungsi kedua untuk menguji apakah Repository melakukan request http kalau Local Data Store null. Terakhir untuk menguji kalau Repository akan melempar exception ketika terjadi kesalahan saat melakukan request http.
Finally!
Repository kita sudah siap, yay! Cukup banyak yang udah kita bahas tentang Repository. Kita udah mengimplementasikan dua repository dan juga membuat unit test untuk menguji dua repository tersebut. Mungkin kalau kalian sadar, kita tidak menggunakan komponen dari framework Android (Context, Activity, Fragment dll) sama sekali selama kita mengimplementasikan dua repository ini. Ini hal yang bagus.
Waktu membangun sebuah komponen, usahakan biar nggak nyentuh langsung komponen framework Android. Ini memudahkan kita untuk membuat unit test dari komponen tersebut. Kalau emang terpaksa harus menggunakan komponen framework Android, lakukan Dependency Inversion Principle seperti yang kita lakukan pada PokemonSetDataStore dan PokemonCardDataStore.
Parts
Artikel ini akan dibagi ke dalam beberapa bagian. Karena saya rasa memahami tugas dari setiap komponen jauh lebih penting daripada hanya sekedar tau bagaimana MVVM diimplementasikan. Bagian-bagian tersebut adalah sebagai berikut:
- Part 1 Pengenalan MVVM
- Part 2 Repository Sumber Data Kita
- Part 3 ViewModel Si Pintar
- Part 4 View Yang Sangat Reaktif
- Part 5 RoomDataStore