Pengembangan Android modern di Kotlin. Bagian 2

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.

gambar

β€œ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:

  1. Android Studio 3 beta 1 Bagian 1
  2. Bahasa Pemrograman Kotlin Bagian 1
  3. Opsi Bangun Bagian 1
  4. ConstraintLayout Bagian 1
  5. Perpustakaan Binding Data Bagian 1
  6. Arsitektur MVVM + Repositori + Pola Pembungkus Manajer Android
  7. RxJava2 dan bagaimana ini membantu kita dalam arsitektur Bagian 3
  8. Belati 2.11, apa itu injeksi ketergantungan, mengapa Anda harus menggunakan Bagian 4 ini
  9. Retrofit (dengan Rx Java2)
  10. 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:

  • MVP
  • MVVM

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.kt
class 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:

Siklus hidup aktivitas
gambar

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:

Lingkup siklus hidup
gambar

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") //To change body of created functions use File | Settings | File Templates. } } 


Jalankan aplikasi


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") //To change body of created functions use File | Settings | File Templates. } } 


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


Skema
gambar

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>){ //todo save repositories in DB } } interface OnRepoLocalReadyCallback { fun onLocalDataReady(data: 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.

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


All Articles