Halo, Habr! Saya hadir untuk Anda terjemahan artikel "
Pengembangan Android modern dengan Kotlin (Bagian 2) " oleh Mladen Rakonjac.
Catatan Artikel ini adalah terjemahan dari serangkaian artikel dari Mladen Rakonjac , tanggal artikel: 09/23/2017. Github Mulai membaca bagian pertama dari SemperPeritus, saya menemukan bahwa bagian yang lain tidak diterjemahkan karena beberapa alasan. Karena itu, saya membawa perhatian Anda pada bagian kedua. Artikel itu ternyata banyak sekali.
βSangat sulit untuk menemukan satu proyek yang akan mencakup semua yang baru dalam pengembangan untuk Android di Android Studio 3.0, jadi saya memutuskan untuk menulisnya.β
Dalam artikel ini, kami akan menganalisis hal berikut:
- Android Studio 3 beta 1 Bagian 1
- Bahasa Pemrograman Kotlin Bagian 1
- Opsi Bangun Bagian 1
- ConstraintLayout Bagian 1
- Perpustakaan Binding Data Bagian 1
- Arsitektur MVVM + Repositori + Pola Pembungkus Manajer Android
- RxJava2 dan bagaimana ini membantu kita dalam arsitektur Bagian 3
- Belati 2.11, apa itu injeksi ketergantungan, mengapa Anda harus menggunakan Bagian 4 ini
- Retrofit (dengan Rx Java2)
- Kamar (dengan Rx Java2)
Arsitektur MVVM + Repositori + Pola Pembungkus Manajer Android
Beberapa kata tentang Arsitektur di dunia Android
Untuk beberapa waktu, pengembang android belum menggunakan arsitektur apa pun dalam proyek mereka. Selama tiga tahun terakhir, banyak hype muncul di komunitas pengembang android. Waktu Kegiatan Tuhan telah berlalu dan Google telah menerbitkan repositori
Arsitektur Arsitektur Android , dengan banyak contoh dan instruksi tentang berbagai pendekatan arsitektur. Akhirnya, di Google IO '17, mereka memperkenalkan
Android Architecture Components , koleksi perpustakaan yang dirancang untuk membantu kami membuat kode yang lebih bersih dan meningkatkan aplikasi.
Komponen mengatakan Anda dapat menggunakan semuanya, atau hanya salah satunya. Namun, saya menemukan semuanya sangat berguna. Lebih lanjut dalam teks dan di bagian berikut ini kami akan menggunakannya. Pertama, saya akan membahas masalah dalam kode, dan kemudian saya akan refactor menggunakan komponen dan pustaka ini untuk melihat masalah apa yang seharusnya mereka pecahkan.
Ada dua
pola arsitektur utama yang membagikan kode GUI:
Sulit mengatakan mana yang lebih baik. Anda harus mencoba keduanya dan memutuskan. Saya lebih suka
MVVM menggunakan komponen yang sadar siklus dan saya akan menulis tentang hal itu. Jika Anda belum pernah mencoba menggunakan MVP, ada banyak artikel bagus di Media tentang hal ini.
Apa itu pola MVVM?
MVVM adalah
pola arsitektur yang mengembang sebagai Model-View-ViewModel. Saya pikir nama ini membingungkan para pengembang. Jika saya adalah orang yang muncul dengan namanya, saya akan menyebutnya View-ViewModel-Model, karena
ViewModel berada di tengah, menghubungkan
View dan
Model .
Tampilan adalah abstraksi untuk
Aktivitas ,
Fragmen, atau Tampilan kustom lainnya (
Tampilan Kustom Android ). Harap perhatikan bahwa penting untuk tidak mengacaukan
Tampilan ini dengan Tampilan Android.
Lihat harus bodoh, kita tidak boleh menulis logika apa pun untuk itu.
Tampilan tidak boleh berisi data. Seharusnya menyimpan referensi ke instance
ViewModel dan semua data yang dibutuhkan
View berasal dari sana. Selain itu,
View harus memperhatikan data ini dan tata letak harus berubah ketika data dari
ViewModel berubah. Untuk meringkas,
View bertanggung jawab untuk yang berikut: tampilan tata letak untuk berbagai data dan status.
ViewModel adalah nama abstrak untuk kelas yang berisi data dan logika, kapan data ini harus diterima dan saat ditampilkan.
ViewModel menyimpan
keadaan saat ini.
ViewModel juga menyimpan tautan ke satu atau lebih
Model dan menerima semua data dari mereka. Dia seharusnya tidak tahu, misalnya, dari mana data berasal, dari database atau dari server. Selain itu,
ViewModel tidak perlu tahu apa-apa tentang
View . Selain itu,
ViewModel seharusnya tidak tahu apa-apa tentang kerangka kerja Android sama sekali.Model adalah nama abstrak untuk lapisan yang menyiapkan data untuk
ViewModel . Ini adalah kelas di mana kami akan menerima data dari server dan menyimpannya, atau menyimpannya di basis data lokal. Perhatikan bahwa ini bukan kelas yang sama dengan Pengguna, Mobil, Kotak, kelas model lain yang hanya menyimpan data. Sebagai aturan, ini adalah implementasi dari template Repositori, yang akan kami pertimbangkan nanti.
Model seharusnya tidak tahu apa-apa tentang
ViewModel .
MVVM , jika diterapkan dengan benar, adalah cara yang bagus untuk memecahkan kode Anda dan membuatnya lebih dapat diuji. Ini membantu kita untuk mengikuti prinsip-prinsip
SOLID , sehingga kode kita lebih mudah dipelihara.
Contoh kode
Sekarang saya akan menulis contoh sederhana yang menunjukkan cara kerjanya.
Untuk memulai, mari buat
Model sederhana yang mengembalikan garis:
RepoModel.ktclass RepoModel { fun refreshData() : String { return "Some new data" } }
Biasanya, menerima data adalah panggilan yang tidak
sinkron , jadi kita harus menunggu. Untuk mensimulasikan ini, saya mengubah kelas menjadi yang berikut:
RepoModel.kt class RepoModel { fun refreshData(onDataReadyCallback: OnDataReadyCallback) { Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000) } } interface OnDataReadyCallback { fun onDataReady(data : String) }
Saya membuat antarmuka
OnDataReadyCallback
dengan metode
onDataReady
. Dan sekarang metode
refreshData
mengimplementasikan (mengimplementasikan)
OnDataReadyCallback
. Untuk mensimulasikan penantian, saya menggunakan
Handler
. Sekali
onDataReady
2 detik, metode
onDataReady
akan dipanggil pada kelas yang mengimplementasikan antarmuka
OnDataReadyCallback
.
Mari kita buat
ViewModel :
MainViewModel.kt class MainViewModel { var repoModel: RepoModel = RepoModel() var text: String = "" var isLoading: Boolean = false }
Seperti yang Anda lihat, ada instance
RepoModel
,
text
, yang akan ditampilkan, dan variabel
isLoading
, yang menyimpan keadaan saat ini. Mari kita buat metode
refresh
yang bertanggung jawab untuk mengambil data:
MainViewModel.kt class MainViewModel { ... val onDataReadyCallback = object : OnDataReadyCallback { override fun onDataReady(data: String) { isLoading.set(false) text.set(data) } } fun refresh(){ isLoading.set(true) repoModel.refreshData(onDataReadyCallback) } }
Metode
refresh
memanggil
refreshData
di
RepoModel
, yang mengambil implementasi
OnDataReadyCallback
dalam argumen. Ok, tapi apa itu
object
? Setiap kali Anda ingin mengimplementasikan antarmuka atau mewarisi kelas extended tanpa subclassing, Anda akan menggunakan
deklarasi objek . Dan jika Anda ingin menggunakan ini sebagai kelas anonim? Dalam hal ini, Anda menggunakan
ekspresi objek :
MainViewModel.kt class MainViewModel { var repoModel: RepoModel = RepoModel() var text: String = "" var isLoading: Boolean = false fun refresh() { repoModel.refreshData( object : OnDataReadyCallback { override fun onDataReady(data: String) { text = data }) } }
Saat kami memanggil
refresh
, kami harus mengubah tampilan ke status
pemuatan dan ketika data tiba, atur
isLoading
ke
false
.
Kami juga harus mengganti
text
dengan
ObservableField<String>
, dan
isLoading
aktif
ObservableField<Boolean>
.
ObservableField
adalah kelas dari perpustakaan Binding Data yang bisa kita gunakan alih-alih membuat objek yang bisa diamati. Ini membungkus objek yang ingin kita amati.
MainViewModel.kt class MainViewModel { var repoModel: RepoModel = RepoModel() val text = ObservableField<String>() val isLoading = ObservableField<Boolean>() fun refresh(){ isLoading.set(true) repoModel.refreshData(object : OnDataReadyCallback { override fun onDataReady(data: String) { isLoading.set(false) text.set(data) } }) } }
Perhatikan bahwa saya menggunakan
val bukan
var , karena kami hanya akan mengubah nilai di bidang, tetapi bukan bidang itu sendiri. Dan jika Anda ingin menginisialisasi, gunakan yang berikut:
initobserv.kt val text = ObservableField("old data") val isLoading = ObservableField(false)
Mari kita ubah tata letak kita sehingga bisa mengamati
teks dan
isLoading . Untuk memulai, ikat
MainViewModel alih-alih
Repositori :
activity_main.xml <data> <variable name="viewModel" type="me.mladenrakonjac.modernandroidapp.MainViewModel" /> </data>
Lalu:
- Ubah TextView untuk mengamati teks dari MainViewModel
- Tambahkan ProgressBar, yang hanya akan terlihat jika isLoading true
- Tambahkan Tombol, yang, ketika diklik, akan memanggil metode penyegaran dari MainViewModel dan hanya akan dapat diklik jika isinya salah
main_activity.xml ... <TextView android:id="@+id/repository_name" android:text="@{viewModel.text}" ... /> ... <ProgressBar android:id="@+id/loading" android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" ... /> <Button android:id="@+id/refresh_button" android:onClick="@{() -> viewModel.refresh()}" android:clickable="@{viewModel.isLoading ? false : true}" /> ...
Jika Anda menjalankan sekarang, Anda akan mendapatkan kesalahan
View.VISIBLE and View.GONE cannot be used if View is not imported
. Baiklah, mari kita impor:
main_activity.xml <data> <import type="android.view.View"/> <variable name="viewModel" type="me.fleka.modernandroidapp.MainViewModel" /> </data>
Ok, selesai dengan tata letak. Sekarang akhiri dengan penjilidan. Seperti yang saya katakan,
View
harus memiliki instance dari
ViewModel
:
MainActivity.kt class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding var mainViewModel = MainViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = mainViewModel binding.executePendingBindings() } }
Akhirnya, kita bisa lari
Anda dapat melihat bahwa
data lama diganti dengan
data baru .
Ini adalah contoh MVVM sederhana.
Tapi ada satu masalah, mari kita putar layar
data lama menggantikan
data baru . Bagaimana ini mungkin? Lihatlah siklus hidup Aktivitas:
Ketika Anda menghidupkan telepon, instance baru Activity dibuat dan metode
onCreate()
dipanggil. Lihatlah aktivitas kami:
MainActivity.kt class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding var mainViewModel = MainViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = mainViewModel binding.executePendingBindings() } }
Seperti yang Anda lihat, ketika instance Activity dibuat, instance
MainViewModel dibuat juga. Apakah baik jika kita memiliki instance
MainViewModel yang sama untuk setiap
MainActivity yang dibuat ulang?
Pengantar komponen yang menyadari Siklus Hidup
Karena banyak pengembang dihadapkan dengan masalah ini, pengembang dari Tim Kerangka Android memutuskan untuk membuat perpustakaan yang dirancang untuk membantu menyelesaikan masalah ini. Kelas
ViewModel adalah salah satunya. Ini adalah kelas yang harus diwarisi oleh semua Model View kami.
Mari kita mewarisi
MainViewModel dari
ViewModel dari komponen yang sadar siklus. Pertama, kita perlu menambahkan pustaka
komponen yang sadar siklus hidup ke file
build.gradle kami:
build.gradle dependencies { ... implementation "android.arch.lifecycle:runtime:1.0.0-alpha9" implementation "android.arch.lifecycle:extensions:1.0.0-alpha9" kapt "android.arch.lifecycle:compiler:1.0.0-alpha9"
Jadikan
MainViewModel sebagai pewaris
ViewModel :
MainViewModel.kt package me.mladenrakonjac.modernandroidapp import android.arch.lifecycle.ViewModel class MainViewModel : ViewModel() { ... }
Metode
onCreate () dari MainActivity kami akan terlihat seperti ini:
MainActivity.kt class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.executePendingBindings() } }
Perhatikan bahwa kami tidak membuat instance baru dari
MainViewModel . Kami akan mendapatkannya menggunakan
ViewModelProviders .
ViewModelProviders adalah kelas Utilitas yang memiliki metode untuk mendapatkan
ViewModelProvider . Ini semua tentang
ruang lingkup . Jika Anda memanggil
ViewModelProviders.of (ini) dalam suatu Kegiatan, maka
ViewModel Anda akan hidup selama Aktivitas ini hidup (sampai dihancurkan tanpa membuat ulang). Oleh karena itu, jika Anda menyebut ini dalam fragmen, maka
ViewModel Anda akan hidup saat Fragmen itu hidup, dll. Lihatlah diagram:
ViewModelProvider bertanggung jawab untuk membuat instance baru di panggilan pertama atau mengembalikan yang lama jika Aktivitas atau Fragmen Anda dibuat ulang.
Jangan bingung dengan
MainViewModel::class.java
Di Kotlin, jika Anda ikuti
MainViewModel::class
ini akan mengembalikan Anda
KClass , yang tidak sama dengan Kelas dari Jawa. Jadi jika kita menulis
.java , maka menurut dokumentasi itu adalah:
Akan mengembalikan instance dari Java Class yang sesuai dengan instance KClass ini
Mari kita lihat apa yang terjadi ketika Anda memutar layar
Kami memiliki data yang sama seperti sebelum rotasi layar.
Pada artikel terakhir, saya mengatakan bahwa aplikasi kita akan mendapatkan daftar repositori Github dan menunjukkannya. Untuk melakukan ini, kita harus menambahkan fungsi
getRepositories , yang akan mengembalikan daftar repositori palsu:
RepoModel.kt class RepoModel { fun refreshData(onDataReadyCallback: OnDataReadyCallback) { Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000) } fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First", "Owner 1", 100 , false)) arrayList.add(Repository("Second", "Owner 2", 30 , true)) arrayList.add(Repository("Third", "Owner 3", 430 , false)) Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) },2000) } } interface OnDataReadyCallback { fun onDataReady(data : String) } interface OnRepositoryReadyCallback { fun onDataReady(data : ArrayList<Repository>) }
Kita juga perlu memiliki metode di
MainViewModel yang memanggil
getRepositori dari
RepoModel :
MainViewModel.kt class MainViewModel : ViewModel() { ... var repositories = ArrayList<Repository>() fun refresh(){ ... } fun loadRepositories(){ isLoading.set(true) repoModel.getRepositories(object : OnRepositoryReadyCallback{ override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories = data } }) } }
Dan akhirnya, kita perlu menunjukkan repositori ini di RecyclerView. Untuk melakukan ini, kita harus:
- Buat tata letak rv_item_repository.xml
- Tambahkan RecyclerView ke tata letak activity_main.xml
- Buat RepositoryRecyclerViewAdapter
- Instal adaptor di recyclerview
Untuk membuat
rv_item_repository.xml, saya menggunakan pustaka CardView, jadi kita perlu menambahkannya ke build.gradle (app):
implementation 'com.android.support:cardview-v7:26.0.1'
Begini tampilannya:
rv_item_repository.xml <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="android.view.View" /> <variable name="repository" type="me.mladenrakonjac.modernandroidapp.uimodels.Repository" /> </data> <android.support.v7.widget.CardView android:layout_width="match_parent" android:layout_height="96dp" android:layout_margin="8dp"> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/repository_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:text="@{repository.repositoryName}" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.083" tools:text="Modern Android App" /> <TextView android:id="@+id/repository_has_issues" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="@string/has_issues" android:textStyle="bold" android:visibility="@{repository.hasIssues ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toBottomOf="@+id/repository_name" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toEndOf="@+id/repository_name" app:layout_constraintTop_toTopOf="@+id/repository_name" app:layout_constraintVertical_bias="1.0" /> <TextView android:id="@+id/repository_owner" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:text="@{repository.repositoryOwner}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/repository_name" app:layout_constraintVertical_bias="0.0" tools:text="Mladen Rakonjac" /> <TextView android:id="@+id/number_of_starts" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="@{String.valueOf(repository.numberOfStars)}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/repository_owner" app:layout_constraintVertical_bias="0.0" tools:text="0 stars" /> </android.support.constraint.ConstraintLayout> </android.support.v7.widget.CardView> </layout>
Langkah selanjutnya adalah menambahkan RecyclerView ke
activity_main.xml . Sebelum melakukan ini, pastikan untuk menambahkan perpustakaan RecyclerView:
implementation 'com.android.support:recyclerview-v7:26.0.1'
activity_main.xml <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="android.view.View"/> <variable name="viewModel" type="me.fleka.modernandroidapp.MainViewModel" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context="me.fleka.modernandroidapp.MainActivity"> <ProgressBar android:id="@+id/loading" android:layout_width="48dp" android:layout_height="48dp" android:indeterminate="true" android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toTopOf="@+id/refresh_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <android.support.v7.widget.RecyclerView android:id="@+id/repository_rv" android:layout_width="0dp" android:layout_height="0dp" android:indeterminate="true" android:visibility="@{viewModel.isLoading ? View.GONE : View.VISIBLE}" app:layout_constraintBottom_toTopOf="@+id/refresh_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:listitem="@layout/rv_item_repository" /> <Button android:id="@+id/refresh_button" android:layout_width="160dp" android:layout_height="40dp" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:onClick="@{() -> viewModel.loadRepositories()}" android:clickable="@{viewModel.isLoading ? false : true}" android:text="Refresh" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="1.0" /> </android.support.constraint.ConstraintLayout> </layout>
Perhatikan bahwa kami menghapus beberapa elemen TextView dan sekarang tombol meluncurkan
loadRepositori alih-alih
menyegarkan :
button.xml <Button android:id="@+id/refresh_button" android:onClick="@{() -> viewModel.loadRepositories()}" ... />
Mari kita hapus metode
refresh dari MainViewModel dan
refreshData dari RepoModel sebagai tidak perlu.
Sekarang Anda perlu membuat Adaptor untuk RecyclerView:
RepositoryRecyclerViewAdapter.kt class RepositoryRecyclerViewAdapter(private var items: ArrayList<Repository>, private var listener: OnItemClickListener) : RecyclerView.Adapter<RepositoryRecyclerViewAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder { val layoutInflater = LayoutInflater.from(parent?.context) val binding = RvItemRepositoryBinding.inflate(layoutInflater, parent, false) return ViewHolder(binding) } override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position], listener) override fun getItemCount(): Int = items.size interface OnItemClickListener { fun onItemClick(position: Int) } class ViewHolder(private var binding: RvItemRepositoryBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(repo: Repository, listener: OnItemClickListener?) { binding.repository = repo if (listener != null) { binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) }) } binding.executePendingBindings() } } }
Perhatikan bahwa ViewHolder mengambil instance dari tipe
RvItemRepositoryBinding , alih-alih
View , sehingga kita bisa mengimplementasikan Data Binding di ViewHolder untuk setiap elemen. Jangan malu dengan fungsi single line (oneline):
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position], listener)
Ini hanya entri singkat untuk:
override fun onBindViewHolder(holder: ViewHolder, position: Int){ return holder.bind(items[position], listener) }
Dan
item [posisi] adalah implementasi untuk operator indeks. Ini mirip dengan
items.get (posisi) .
Baris lain yang mungkin membingungkan Anda:
binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) })
Anda dapat mengganti parameter dengan _ jika Anda tidak menggunakannya. Bagus ya
Kami membuat adaptor, tetapi masih belum menerapkannya ke
recyclerView di
MainActivity :
MainActivity.kt class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener { lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.viewModel = viewModel binding.executePendingBindings() binding.repositoryRv.layoutManager = LinearLayoutManager(this) binding.repositoryRv.adapter = RepositoryRecyclerViewAdapter(viewModel.repositories, this) } override fun onItemClick(position: Int) { TODO("not implemented")
Ini aneh. Apa yang terjadi
- Aktivitas dibuat, jadi adaptor baru juga dibuat dengan repositori yang hampir kosong
- Kami menekan tombol
- Disebut loadRepositori, menunjukkan kemajuan
- Setelah 2 detik, kita mendapatkan repositori, progres disembunyikan, tetapi mereka tidak muncul. Ini karena notifyDataSetChanged tidak dipanggil di adaptor
- Ketika kita memutar layar, Aktivitas baru dibuat, jadi adaptor baru dibuat dengan parameter repositori dengan beberapa data
Jadi, karena
MainViewModel harus memberi tahu
MainActivity tentang item baru, dapatkah kita memanggil
notifyDataSetChanged ?
Kami tidak bisa.
Ini sangat penting,
MainViewModel tidak harus tahu tentang
MainActivity sama sekali .
MainActivity adalah orang yang memiliki instance
MainViewModel , jadi ia harus mendengarkan perubahan dan memberi tahu
Adaptor tentang perubahan tersebut.
Tetapi bagaimana cara melakukannya?
Kita dapat mengamati
repositori , jadi setelah mengubah data kita dapat mengubah adaptor kita.
Apa yang salah dengan keputusan ini?
Mari kita lihat kasus berikut:
- Di MainActivity, kami mengamati repositori: ketika perubahan terjadi, kami menjalankan notifyDataSetChanged
- Kami menekan tombol
- Sementara kami menunggu perubahan data, MainActivity dapat dibuat ulang karena perubahan konfigurasi.
- MainViewModel kami masih hidup
- Setelah 2 detik, bidang repositori menerima item baru dan memberi tahu pengamat bahwa data telah berubah
- Pengamat mencoba mengeksekusi notifyDataSetChanged pada adaptor , yang tidak ada lagi, karena MainActivity telah diciptakan kembali
Yah, keputusan kami tidak cukup baik.
Pengantar LiveData
LiveData adalah
komponen yang menyadari Siklus Hidup lainnya
. Hal ini didasarkan pada pengamatan yang mengetahui tentang siklus hidup Lihat. Jadi ketika suatu Aktivitas dihancurkan karena
perubahan konfigurasi ,
LiveData mengetahuinya, sehingga menghapus pengamat dari Aktivitas yang hancur juga.
Kami menerapkan di
MainViewModel :
MainViewModel.kt class MainViewModel : ViewModel() { var repoModel: RepoModel = RepoModel() val text = ObservableField("old data") val isLoading = ObservableField(false) var repositories = MutableLiveData<ArrayList<Repository>>() fun loadRepositories() { isLoading.set(true) repoModel.getRepositories(object : OnRepositoryReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories.value = data } }) } }
dan mulai mengamati MainActivity:
MainActivity.kt class MainActivity : LifecycleActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener { private lateinit var binding: ActivityMainBinding private val repositoryRecyclerViewAdapter = RepositoryRecyclerViewAdapter(arrayListOf(), this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.viewModel = viewModel binding.executePendingBindings() binding.repositoryRv.layoutManager = LinearLayoutManager(this) binding.repositoryRv.adapter = repositoryRecyclerViewAdapter viewModel.repositories.observe(this, Observer<ArrayList<Repository>> { it?.let{ repositoryRecyclerViewAdapter.replaceData(it)} }) } override fun onItemClick(position: Int) { TODO("not implemented")
Apa arti kata
itu ? Jika suatu fungsi hanya memiliki satu parameter, maka akses ke parameter ini dapat diperoleh dengan menggunakan kata kunci itu. Jadi, misalkan kita memiliki ekspresi lambda untuk dikalikan dengan 2:
((a) -> 2 * a)
Dapat diganti sebagai berikut:
(it * 2)
Jika Anda memulai aplikasi sekarang, Anda dapat memastikan semuanya bekerja
...
Mengapa saya lebih suka MVVM daripada MVP?
- Tidak ada antarmuka yang membosankan untuk Tampilan, karena ViewModel tidak memiliki referensi untuk Lihat
- Tidak ada antarmuka yang membosankan untuk Presenter, dan ini tidak perlu
- Jauh lebih mudah untuk menangani perubahan konfigurasi
- Menggunakan MVVM, kami memiliki lebih sedikit kode untuk Aktivitas, Fragmen dll.
...
Pola penyimpanan
Seperti yang saya katakan sebelumnya,
Model hanyalah nama abstrak untuk layer di mana kami sedang menyiapkan data. Biasanya berisi repositori dan kelas data. Setiap entitas (data) kelas memiliki kelas
Repositori yang sesuai. Sebagai contoh, jika kita memiliki kelas
User dan
Post , kita juga harus memiliki
UserRepository dan
PostRepository . Semua data berasal dari sana. Kita seharusnya tidak pernah memanggil instance Shared Preferences atau DB dari View atau ViewModel.
Jadi kita dapat mengganti nama RepoModel menjadi
GitRepoRepository , di mana
GitRepo akan berasal dari repositori Github dan
Repositori akan berasal dari pola Repositori.
RepoRepositories.kt class GitRepoRepository { fun getGitRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First", "Owner 1", 100, false)) arrayList.add(Repository("Second", "Owner 2", 30, true)) arrayList.add(Repository("Third", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) }, 2000) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) }
Oke,
MainViewModel mendapatkan daftar repositori Github dari
GitRepoRepsitories , tetapi dari mana mendapatkan
GitRepoRepositori dari ?
Anda dapat memanggil
klien atau
DB secara langsung di repositori dari instance, tetapi itu masih bukan praktik terbaik. Aplikasi Anda harus modular sebanyak yang Anda bisa. Bagaimana jika Anda memutuskan untuk menggunakan klien yang berbeda untuk mengganti Volley dengan Retrofit? Jika Anda memiliki semacam logika di dalamnya, akan sulit untuk melakukan refactoring. Repositori Anda tidak perlu tahu klien mana yang Anda gunakan untuk mengambil data jarak jauh.
- Satu-satunya hal yang perlu diketahui repositori adalah bahwa data tiba dari jauh atau lokal. Tidak perlu tahu bagaimana kita mendapatkan data jarak jauh atau lokal ini.
- Satu-satunya model tampilan yang diperlukan adalah data
- Satu-satunya hal yang harus dilakukan Tampilan adalah menampilkan data ini.
Ketika saya baru mulai mengembangkan Android, saya bertanya-tanya bagaimana aplikasi bekerja offline dan bagaimana sinkronisasi data bekerja. Arsitektur aplikasi yang baik memungkinkan kita melakukan ini dengan mudah. Misalnya, ketika
loadRepositories di
ViewModel dipanggil jika ada koneksi Internet,
GitRepoRepositories dapat menerima data dari sumber data jarak jauh dan menyimpannya ke sumber data lokal. Ketika ponsel sedang offline,
GitRepoRepository dapat menerima data dari penyimpanan lokal. Jadi,
Repositori harus memiliki instance
RemoteDataSource dan
LocalDataSource dan pemrosesan logika dari mana data ini berasal.
Tambahkan
sumber data lokal :
GitRepoLocalDataSource.kt class GitRepoLocalDataSource { fun getRepositories(onRepositoryReadyCallback: OnRepoLocalReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First From Local", "Owner 1", 100, false)) arrayList.add(Repository("Second From Local", "Owner 2", 30, true)) arrayList.add(Repository("Third From Local", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onLocalDataReady(arrayList) }, 2000) } fun saveRepositories(arrayList: ArrayList<Repository>){
Di sini kita memiliki dua metode: yang pertama, yang mengembalikan data lokal palsu, dan yang kedua, untuk penyimpanan data fiktif.Tambahkan sumber data jarak jauh :GitRepoRemoteDataSource.kt class GitRepoRemoteDataSource { fun getRepositories(onRepositoryReadyCallback: OnRepoRemoteReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First from remote", "Owner 1", 100, false)) arrayList.add(Repository("Second from remote", "Owner 2", 30, true)) arrayList.add(Repository("Third from remote", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onRemoteDataReady(arrayList) }, 2000) } } interface OnRepoRemoteReadyCallback { fun onRemoteDataReady(data: ArrayList<Repository>) }
Hanya ada satu metode yang mengembalikan data jarak jauh palsu .Sekarang kita dapat menambahkan beberapa logika ke repositori kami:GitRepoRepository.kt class GitRepoRepository { val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { remoteDataSource.getRepositories( object : OnRepoRemoteReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) }
Dengan demikian, berbagi sumber, kami dengan mudah menyimpan data secara lokal.Bagaimana jika Anda hanya membutuhkan data dari jaringan, Anda masih perlu menggunakan templat repositori? Ya
Ini membuat pengujian kode lebih mudah, pengembang lain dapat lebih memahami kode Anda, dan Anda dapat mendukungnya lebih cepat!...
Pembungkus Manajer Android
Bagaimana jika Anda ingin memeriksa koneksi internet Anda di GitRepoRepository untuk mengetahui dari mana meminta data? Kami sudah mengatakan bahwa kami tidak boleh menempatkan kode terkait Android di ViewModel dan Model , jadi bagaimana cara mengatasi masalah ini?Mari kita menulis bungkus untuk koneksi internet:NetManager.kt (Solusi serupa berlaku untuk manajer lain, misalnya, untuk NfcManager) class NetManager(private var applicationContext: Context) { private var status: Boolean? = false val isConnectedToInternet: Boolean? get() { val conManager = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val ni = conManager.activeNetworkInfo return ni != null && ni.isConnected } }
Kode ini hanya akan berfungsi jika kami menambahkan izin ke manifes: <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
Tapi bagaimana untuk membuat sebuah contoh di Repository, jika kita tidak memiliki konteks ( konteks The )? Kami dapat memintanya di konstruktor:GitRepoRepository.kt class GitRepoRepository (context: Context){ val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() val netManager = NetManager(context) fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) }
Kami membuat sebelum instance GitRepoRepository baru di ViewModel. Bagaimana kita sekarang dapat memiliki NetManager di ViewModel ketika kita membutuhkan konteks untuk NetManager ? Anda dapat menggunakan AndroidViewModel dari pustaka komponen sadar-siklus, yang memiliki konteks . Ini adalah konteks aplikasi, bukan Activity.MainViewModel.kt class MainViewModel : AndroidViewModel { constructor(application: Application) : super(application) var gitRepoRepository: GitRepoRepository = GitRepoRepository(NetManager(getApplication())) val text = ObservableField("old data") val isLoading = ObservableField(false) var repositories = MutableLiveData<ArrayList<Repository>>() fun loadRepositories() { isLoading.set(true) gitRepoRepository.getRepositories(object : OnRepositoryReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories.value = data } }) } }
Di baris ini constructor(application: Application) : super(application)
kami mendefinisikan konstruktor untuk MainViewModel . Ini diperlukan karena AndroidViewModel meminta instance aplikasi dalam konstruktornya. Jadi, di konstruktor kami, kami memanggil metode super yang memanggil konstruktor AndroidViewModel , dari mana kami mewarisi.Catatan: kita dapat menyingkirkan satu baris jika kita melakukannya: class MainViewModel(application: Application) : AndroidViewModel(application) { ... }
Dan sekarang setelah kita memiliki instance NetManager di GitRepoRepository , kita dapat memeriksa koneksi internet:GitRepoRepository.kt class GitRepoRepository(val netManager: NetManager) { val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { netManager.isConnectedToInternet?.let { if (it) { remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback { override fun onRemoteDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } else { localDataSource.getRepositories(object : OnRepoLocalReadyCallback { override fun onLocalDataReady(data: ArrayList<Repository>) { onRepositoryReadyCallback.onDataReady(data) } }) } } } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) }
Jadi, jika kita memiliki koneksi Internet, kita akan menerima data yang dihapus dan menyimpannya secara lokal. Jika kami tidak memiliki koneksi Internet, kami akan mendapatkan data lokal.Catatan Kotlin : pernyataan let memeriksa null dan mengembalikan nilai di dalamnya .Dalam salah satu artikel berikut ini saya akan menulis tentang injeksi ketergantungan, betapa buruknya membuat instance repositori di ViewModel, dan bagaimana cara menghindari menggunakan AndroidViewModel. Saya juga akan menulis tentang sejumlah besar masalah yang sekarang ada dalam kode kita. Saya meninggalkan mereka karena suatu alasan ...Saya mencoba menunjukkan kepada Anda masalah sehingga Anda dapat memahami mengapa semua perpustakaan ini populer dan mengapa Anda harus menggunakannya.PS Saya telah berubah pikiran tentang mapper ( pembuat peta ). Saya memutuskan untuk membahas hal ini di artikel-artikel berikut.