Arsitektur dan desain aplikasi Android (pengalaman saya)

Habr, halo!

Hari ini saya ingin berbicara tentang arsitektur yang saya ikuti di aplikasi Android saya. Saya menggunakan Arsitektur Bersih sebagai dasarnya, dan menggunakan Komponen Arsitektur Android (ViewModel, LiveData, LiveEvent) + Kotlin Coroutines sebagai alat. Terlampir adalah kode contoh fiktif yang tersedia di GitHub .

Penolakan


Saya ingin berbagi pengalaman pengembangan saya, saya sama sekali tidak berpura-pura bahwa solusi saya adalah satu-satunya yang benar dan tanpa kekurangan. Arsitektur aplikasi adalah jenis model yang kami pilih untuk menyelesaikan masalah tertentu, dan untuk model yang dipilih, kecukupannya dalam menerapkan tugas tertentu adalah penting.

Masalah: mengapa kita membutuhkan arsitektur?


Sebagian besar proyek yang saya ikuti memiliki masalah yang sama: logika aplikasi ditempatkan di dalam lingkungan android, yang mengarah ke sejumlah besar kode di dalam Fragmen dan Aktivitas. Dengan demikian, kode dikelilingi oleh dependensi yang tidak diperlukan sama sekali, pengujian unit menjadi hampir tidak mungkin, serta digunakan kembali. Fragmen menjadi objek Tuhan dari waktu ke waktu, bahkan perubahan kecil menyebabkan kesalahan, mendukung proyek menjadi mahal dan mahal secara emosional.

Ada proyek yang tidak memiliki arsitektur sama sekali (semuanya jelas di sini, tidak ada pertanyaan untuk mereka), ada proyek dengan klaim arsitektur, tetapi masalah yang sama persis muncul di sana. Sekarang modis untuk menggunakan Arsitektur Bersih di Android. Saya sering melihat bahwa Arsitektur Bersih terbatas untuk membuat repositori dan skrip yang memanggil repositori ini dan tidak melakukan hal lain. Lebih buruk lagi: skrip tersebut mengembalikan model dari repositori yang disebut. Dan dalam arsitektur seperti itu tidak ada artinya sama sekali. Dan karena Karena skrip hanya memanggil repositori yang diperlukan, seringkali logika bertumpu pada ViewModel atau, lebih buruk lagi, menetap dalam fragmen dan aktivitas. Semua ini kemudian berubah menjadi berantakan, tidak bisa menerima pengujian otomatis.

Tujuan arsitektur dan desain


Tujuan arsitektur adalah untuk memisahkan logika bisnis kami dari detail. Maksud saya, misalnya, API eksternal (saat kami mengembangkan klien untuk layanan REST), Android - lingkungan (UI, layanan), dll. Pada intinya, saya menggunakan arsitektur Clean, tetapi dengan asumsi implementasi saya.

Tujuan dari desain adalah untuk menyatukan UI, API, Business logic, model sehingga semua ini cocok untuk pengujian otomatis, longgar digabungkan, dan mudah diperluas. Dalam desain, saya menggunakan Komponen Arsitektur Android.

Bagi saya, arsitektur harus memenuhi kriteria berikut:

  1. UI sesederhana mungkin dan hanya memiliki tiga fungsi:
  2. Sajikan data kepada pengguna. Data siap ditampilkan. Ini adalah fungsi utama dari UI. Berikut adalah widget, animasi, fragmen, dll.
  3. . ViewModel LiveData.
  4. . framework, . .
  5. - . .


Diagram skema arsitektur ditunjukkan pada gambar di bawah ini:

gambar

Kami bergerak dari bawah ke atas berlapis-lapis, dan lapisan yang di bawah tidak tahu apa-apa tentang lapisan di atas. Dan lapisan atas hanya merujuk ke lapisan yang satu tingkat lebih rendah. Itu Lapisan API tidak dapat merujuk ke domain.

Lapisan domain berisi entitas bisnis dengan logikanya sendiri. Biasanya ada entitas yang ada tanpa aplikasi. Misalnya, untuk bank, mungkin ada entitas pinjaman dengan logika kompleks untuk menghitung bunga, dll.

Lapisan logika aplikasi berisi skrip untuk aplikasi itu sendiri. Di sinilah semua koneksi aplikasi ditentukan, esensinya dibangun.

Lapisan api, android hanyalah implementasi spesifik dari aplikasi kita di lingkungan Android. Idealnya, lapisan ini dapat diubah menjadi apa saja.

, , โ€” . . 2- . , . . TDD , . Android, API ..

Android-.

gambar

Jadi, lapisan logika adalah kuncinya, itu adalah aplikasi. Hanya lapisan logika yang bisa merujuk dan berinteraksi dengan domain. Selain itu, lapisan logika berisi antarmuka yang memungkinkan logika untuk berinteraksi dengan detail aplikasi (api, android, dll.). Inilah yang disebut prinsip inversi dependensi, yang memungkinkan logika untuk tidak bergantung pada detail, tetapi sebaliknya. Lapisan logika berisi skenario penggunaan aplikasi (Use Cases), yang beroperasi pada data yang berbeda, berinteraksi dengan domain, repositori, dll. Dalam pengembangan, saya suka berpikir dalam skrip. Untuk setiap tindakan atau peristiwa pengguna dari sistem, skrip tertentu diluncurkan yang memiliki parameter input dan output, serta hanya satu metode - untuk menjalankan skrip.

Seseorang memperkenalkan konsep tambahan suatu interaksor, yang dapat menggabungkan beberapa skenario penggunaan dan menghasilkan logika tambahan. Tapi saya tidak melakukan ini, saya percaya bahwa setiap skrip dapat memperluas atau memasukkan skrip lain, ini tidak memerlukan interaktor. Jika Anda melihat skema UML, maka Anda dapat melihat koneksi penyertaan dan ekstensi.

Skema umum aplikasi adalah sebagai berikut:

gambar

  1. Lingkungan android dibuat (aktivitas, fragmen, dll.).
  2. ViewModel dibuat (satu atau lebih).
  3. ViewModel membuat skrip yang diperlukan yang dapat dijalankan dari ViewModel ini. Skenario sebaiknya disuntikkan dengan DI.
  4. Pengguna melakukan suatu tindakan.
  5. Setiap komponen UI dikaitkan dengan perintah yang dapat dijalankan.
  6. Sebuah skrip dijalankan dengan parameter yang diperlukan, misalnya, Login.execute (login, kata sandi).
  7. DI , . ( api, ). . , , , REST JSON . , , . , . , . - . , . , , . , , .
  8. . ViewModel, UI. LiveData (.9 10).

Itu peran kunci yang kita miliki adalah logika dan model datanya. Kami melihat konversi ganda: yang pertama adalah transformasi repositori menjadi model data skenario dan yang kedua adalah konversi ketika skrip memberikan data ke lingkungan sebagai hasil dari kerjanya. Biasanya hasil skrip diberikan ke viewModel untuk ditampilkan di UI. Script harus mengembalikan data yang dengannya viewModel dan UI tidak melakukan hal lain.

Perintah

UI memulai eksekusi skrip menggunakan perintah. Dalam proyek saya, saya menggunakan implementasi perintah saya sendiri, mereka bukan bagian dari komponen arsitektur atau apa pun. Secara umum, implementasinya sederhana, sebagai kenalan yang lebih dalam dengan ide tersebut, Anda dapat melihat implementasi perintah di reactiveui.netuntuk c #. Sayangnya, saya tidak bisa mengeluarkan kode kerja saya, hanya implementasi yang disederhanakan sebagai contoh.

Tugas utama perintah adalah menjalankan beberapa skrip, meneruskan parameter input ke dalamnya, dan setelah eksekusi, mengembalikan hasil perintah (data atau pesan kesalahan). Biasanya, semua perintah dijalankan secara tidak sinkron. Selain itu, tim merangkum metode perhitungan latar belakang. Saya menggunakan coroutine, tetapi mereka mudah untuk diganti dengan RX, dan Anda hanya perlu melakukan ini di banyak abstrak dari kasus perintah + penggunaan. Sebagai bonus, sebuah tim dapat melaporkan statusnya: apakah itu dieksekusi sekarang atau tidak dan apakah itu dapat dieksekusi pada prinsipnya. Tim dengan mudah memecahkan beberapa masalah, misalnya, masalah panggilan ganda (ketika pengguna mengklik tombol beberapa kali saat operasi sedang berjalan) atau masalah visibilitas dan pembatalan.

Contoh


Fitur Implement: masuk ke aplikasi menggunakan login dan kata sandi.
Jendela tersebut harus berisi bidang isian masuk dan kata sandi dan tombol "Masuk". Logika kerja adalah sebagai berikut:

  1. Tombol "Login" harus tidak aktif jika nama pengguna dan kata sandi berisi kurang dari 4 karakter.
  2. Tombol "Login" harus tidak aktif selama proses login.
  3. Selama prosedur login, indikator (pemuat) harus ditampilkan.
  4. Jika login berhasil, pesan selamat datang harus ditampilkan.
  5. Jika nama pengguna dan / atau kata sandi salah, maka pesan kesalahan akan muncul di atas bidang login.
  6. Jika pesan kesalahan ditampilkan di layar, maka setiap input karakter dalam bidang login atau kata sandi akan menghapus pesan ini sampai upaya berikutnya.

Anda dapat memecahkan masalah ini dengan berbagai cara, misalnya, dengan meletakkan semuanya di MainActivity.
Tapi saya selalu mengikuti implementasi dua aturan utama saya:

  1. Logika bisnis tidak tergantung pada detail.
  2. UI sesederhana mungkin. Dia hanya berurusan dengan tugasnya (dia menyajikan data yang ditransfer kepadanya, dan juga menyiarkan perintah dari pengguna).

Seperti inilah bentuk aplikasi:

gambar

MainActivity terlihat seperti ini:

class MainActivity : AppCompatActivity() {

   private val vm: MainViewModel by viewModel()

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       bindLoginView()
       bindProgressBar()

       observeAuthorization()
       observeRefreshView()
   }

   private fun bindProgressBar() {
       progressBar.bindVisibleWithCommandIsExecuting(this, vm.loginCommand)
   }

   private fun bindLoginView() {
       loginEdit.bindAfterTextChangedWithCommand(vm.loginValidityCommand)
       passwordEdit.bindAfterTextChangedWithCommand(vm.passwordValidityCommand)

       loginButton.bindCommand(this, vm.loginCommand) {
           LoginParameters(loginEdit.text.toString(), passwordEdit.text.toString())
       }
   }

   private fun observeAuthorization() {
       vm.authorizationSuccessLive.observe(this, Observer {
           showAuthorizeSuccessMsg(it?.data)
       })
       vm.authorizationErrorLive.observe(this, Observer {
           showAuthorizeErrorMsg()
       })
   }

   private fun observeRefreshView() {
       vm.refreshLoginViewLive.observe(this, Observer {
           hideAuthorizeErrorMsg()
       })
   }

   private fun showAuthorizeErrorMsg() {
       loginErrorMsg.isInvisible = false
   }

   private fun hideAuthorizeErrorMsg() {
       loginErrorMsg.isInvisible = true
   }

   private fun showAuthorizeSuccessMsg(name : String?) {
       val msg = getString( R.string.success_login, name)
       Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
   }
}


Kegiatannya cukup sederhana, aturan UI dijalankan. Saya menulis beberapa ekstensi sederhana, seperti bindVisibleWithCommandIsExecuting, untuk mengaitkan perintah dengan elemen UI dan bukan duplikat kode.

Kode contoh ini dengan komentar tersedia di GitHub , jika tertarik, Anda dapat mengunduh dan membiasakan diri.

Itu saja, terima kasih sudah menonton!

All Articles