Pola modularisasi umum

Tidak ada satu strategi modularisasi yang cocok untuk semua project. Karena sifat Gradle yang fleksibel, tidak banyak batasan terkait cara mengatur project. Halaman ini memberikan ringkasan tentang beberapa aturan umum dan pola lazim yang dapat Anda gunakan saat mengembangkan aplikasi Android multi-modul.

Prinsip kohesi tinggi dan pengaitan rendah

Salah satu cara untuk menunjukkan karakter codebase modular adalah dengan menggunakan properti pengaitan dan kohesi. Pengaitan mengukur sejauh mana modul saling bergantung satu sama lain. Kohesi, dalam konteks ini, mengukur bagaimana elemen modul tunggal terkait secara fungsional. Sebagai aturan umum, Anda harus berusaha mendapatkan pengaitan rendah dan kohesi tinggi:

  • Pengaitan rendah berarti modul harus sebisa mungkin terpisah dari satu sama lain, sehingga perubahan pada satu modul hanya sedikit atau sama sekali tidak berdampak pada modul lainnya. Modul seharusnya tidak memiliki pengetahuan tentang cara kerja internal modul lainnya.
  • Kohesi tinggi berarti modul harus terdiri dari kumpulan kode yang bertindak sebagai sistem. Modul tersebut harus memiliki tanggung jawab yang jelas dan tetap berada dalam batas pengetahuan domain tertentu. Pertimbangkan contoh aplikasi eBook. Mungkin tidak tepat untuk menggabungkan kode terkait buku dan pembayaran dalam modul yang sama karena keduanya adalah dua domain fungsional yang berbeda.

Jenis modul

Cara Anda mengelola modul sebagian besar bergantung pada arsitektur aplikasi Anda. Berikut beberapa jenis modul umum yang dapat Anda perkenalkan di aplikasi sambil mengikuti arsitektur aplikasi yang direkomendasikan.

Modul data

Modul data biasanya berisi repositori, sumber data, dan class model. Tiga tanggung jawab utama modul data adalah:

  1. Mengenkapsulasi semua logika data dan bisnis dari domain tertentu: Setiap modul data harus bertanggung jawab untuk menangani data yang mewakili domain tertentu. Modul data dapat menangani berbagai jenis data selama masih berkaitan.
  2. Mengekspos repositori sebagai API eksternal: API publik modul data harus merupakan repositori karena bertanggung jawab untuk mengekspos data ke seluruh aplikasi.
  3. Menyembunyikan semua detail implementasi dan sumber data dari luar: Sumber data hanya boleh diakses oleh repositori dari modul yang sama. File tetap tersembunyi dari luar. Anda dapat menerapkannya dengan menggunakan kata kunci visibilitas private atau internal Kotlin.
Gambar 1. Contoh modul data dan kontennya.

Modul fitur

Fitur adalah bagian terisolasi dari fungsi aplikasi yang biasanya sesuai dengan layar atau serangkaian layar yang terkait erat, seperti alur pendaftaran atau checkout. Jika aplikasi Anda memiliki navigasi panel bawah, kemungkinan setiap tujuan adalah fitur.

Gambar 2. Setiap tab aplikasi ini dapat ditentukan sebagai fitur.

Fitur dikaitkan dengan layar atau tujuan di aplikasi Anda. Oleh karena itu, fitur kemungkinan memiliki UI terkait dan ViewModel untuk menangani logika dan status. Satu fitur tidak harus dibatasi pada satu tampilan atau tujuan navigasi. Modul fitur bergantung pada modul data.

Gambar 3. Contoh modul fitur dan kontennya.

Modul aplikasi

Modul aplikasi adalah titik entri ke aplikasi. Modul tersebut bergantung pada modul fitur dan biasanya menyediakan navigasi root. Modul aplikasi tunggal dapat dikompilasi ke sejumlah biner yang berbeda berkat varian build.

Gambar 4. Grafik dependensi modul ragam produk *Demo* dan *Full*.

Jika aplikasi Anda menargetkan beberapa jenis perangkat, seperti perangkat otomatis, perangkat Wear, atau TV, tentukan modul aplikasi untuk setiap jenis perangkat. Hal ini membantu memisahkan dependensi khusus platform.

Gambar 5. Grafik dependensi aplikasi Wear.

Modul umum

Modul umum, yang juga dikenal sebagai modul inti, berisi kode yang sering digunakan modul lain. Modul ini mengurangi redundansi dan tidak merepresentasikan lapisan tertentu dalam arsitektur aplikasi. Berikut adalah contoh modul umum:

  • Modul UI: Jika menggunakan elemen UI kustom atau branding yang rumit di aplikasi, sebaiknya Anda mempertimbangkan untuk merangkum koleksi widget ke dalam modul untuk semua fitur yang akan digunakan kembali. Hal ini dapat membantu menjadikan UI Anda konsisten di berbagai fitur. Misalnya, jika tema Anda terpusat, Anda dapat menghindari pemfaktoran ulang yang memusingkan saat terjadi rebranding.
  • Modul analisis: Pelacakan sering kali ditentukan oleh persyaratan bisnis dengan tidak terlalu mempertimbangkan arsitektur software. Pelacak analisis sering digunakan di banyak komponen yang tidak terkait. Jika itu yang terjadi, sebaiknya Anda memiliki modul analisis khusus.
  • Modul jaringan: Jika banyak modul memerlukan koneksi jaringan, Anda dapat mempertimbangkan memiliki modul khusus untuk menyediakan klien http. Hal ini sangat berguna terutama saat klien Anda memerlukan konfigurasi kustom.
  • Modul utilitas: Utilitas, yang juga dikenal sebagai helper, biasanya berupa kode kecil yang digunakan kembali di seluruh aplikasi. Contoh utilitas meliputi helper pengujian, fungsi pemformatan mata uang, validator email, atau operator kustom.

Modul pengujian

Modul pengujian adalah modul Android yang hanya digunakan untuk tujuan pengujian. Modul ini berisi kode pengujian, resource pengujian, dan dependensi pengujian yang hanya diperlukan untuk menjalankan pengujian dan tidak diperlukan selama runtime aplikasi. Modul pengujian dibuat untuk memisahkan kode khusus pengujian dari aplikasi utama, sehingga kode modul lebih mudah diatur dan dikelola.

Kasus penggunaan untuk modul pengujian

Contoh berikut menunjukkan situasi ketika menerapkan modul pengujian dapat sangat bermanfaat:

  • Kode pengujian bersama: Jika Anda memiliki beberapa modul dalam project dan beberapa kode pengujian dapat diterapkan ke lebih dari satu modul, Anda dapat membuat modul pengujian untuk menggunakan kode yang sama. Hal ini dapat membantu mengurangi duplikasi dan membuat kode pengujian Anda lebih mudah dikelola. Kode pengujian bersama dapat mencakup class atau fungsi utilitas, seperti pencocok atau pernyataan kustom, serta data pengujian, seperti respons JSON simulasi.

  • Konfigurasi Build Lebih Bersih: Modul pengujian memungkinkan Anda memiliki konfigurasi build yang lebih bersih, karena modul ini dapat memiliki file build.gradle sendiri. Sebaiknya hindari memenuhi file build.gradle modul aplikasi dengan konfigurasi yang hanya relevan untuk pengujian.

  • Pengujian Integrasi: Modul pengujian dapat digunakan untuk menyimpan pengujian integrasi yang digunakan untuk menguji interaksi antara berbagai bagian aplikasi Anda, termasuk antarmuka pengguna, logika bisnis, permintaan jaringan, dan kueri database.

  • Aplikasi berskala besar: Modul pengujian sangat berguna untuk aplikasi berskala besar dengan codebase yang kompleks dan beberapa modul. Dalam kasus tersebut, modul pengujian dapat membantu meningkatkan pengaturan dan pengelolaan kode.

Gambar 6. Modul pengujian dapat digunakan untuk mengisolasi modul yang saling bergantung satu sama lain.

Komunikasi modul ke modul

Modul jarang ada dalam pemisahan total dan sering kali mengandalkan modul lain dan berkomunikasi dengannya. Penting untuk menjaga agar pengaitan tetap rendah bahkan ketika modul bekerja sama dan sering bertukar informasi. Terkadang, komunikasi langsung antara dua modul tidak diinginkan seperti dalam kasus batasan arsitektur. Hal ini mungkin juga tidak dapat dilakukan, seperti dalam kasus dependensi siklik.

Gambar 7. Komunikasi langsung dua arah antar-modul tidak mungkin dilakukan karena dependensi siklik. Modul mediasi diperlukan untuk mengoordinasikan aliran data antara dua modul independen lainnya.

Untuk mengatasi masalah ini, Anda dapat membuat modul ketiga yang melakukan mediasi antara dua modul lainnya. Modul mediator dapat memproses pesan dari kedua modul dan meneruskannya sesuai kebutuhan. Dalam aplikasi contoh kami, layar checkout perlu mengetahui buku mana yang akan dibeli meskipun peristiwa tersebut berasal dari layar terpisah yang merupakan bagian dari fitur yang berbeda. Dalam hal ini, mediator adalah modul yang memiliki grafik navigasi (biasanya modul aplikasi). Pada contoh, kita menggunakan navigasi untuk meneruskan data dari fitur beranda ke fitur checkout menggunakan komponen Navigation.

navController.navigate("checkout/$bookId")

Tujuan checkout menerima ID buku sebagai argumen yang digunakannya untuk mengambil informasi tentang buku. Anda dapat menggunakan handle status tersimpan untuk mengambil argumen navigasi di dalam ViewModel fitur tujuan.

class CheckoutViewModel(savedStateHandle: SavedStateHandle, …) : ViewModel() {

   val uiState: StateFlow<CheckoutUiState> =
      savedStateHandle.getStateFlow<String>("bookId", "").map { bookId ->
          // produce UI state calling bookRepository.getBook(bookId)
      }
      …
}

Anda tidak boleh meneruskan objek sebagai argumen navigasi. Sebagai gantinya, gunakan ID sederhana yang dapat digunakan fitur untuk mengakses dan memuat resource yang diinginkan dari lapisan data. Dengan cara ini, Anda dapat menjaga agar pengaitan tetap rendah dan tidak melanggar prinsip satu sumber kebenaran.

Dalam contoh di bawah, kedua modul fitur bergantung pada modul data yang sama. Hal ini memungkinkan untuk meminimalkan jumlah data yang diperlukan modul mediator untuk diteruskan dan menjaga pengaitan di antara modul tetap rendah. Daripada meneruskan objek, modul seharusnya bertukar ID dasar dan memuat resource dari modul data bersama.

Gambar 8. Dua modul fitur yang mengandalkan modul data bersama.

Inversi dependensi

Inversi dependensi adalah suatu kondisi saat Anda mengatur kode sedemikian rupa sehingga abstraksi terpisah dari implementasi konkret.

  • Abstraksi: Kontrak yang menentukan cara komponen atau modul dalam aplikasi berinteraksi satu sama lain. Modul abstraksi menentukan API sistem dan berisi antarmuka serta model.
  • Implementasi konkret: Modul yang bergantung pada modul abstraksi dan menerapkan perilaku abstraksi.

Modul yang bergantung pada perilaku yang ditentukan dalam modul abstraksi hanya boleh bergantung pada abstraksi itu sendiri, bukan implementasi tertentu.

Gambar 9. Modul tingkat tinggi tidak bergantung pada modul tingkat rendah secara langsung, sementara modul tingkat tinggi dan modul implementasi bergantung pada modul abstraksi.

Contoh

Misalkan modul fitur memerlukan database agar dapat berfungsi. Modul fitur ini tidak terkait dengan cara implementasi database, baik database Room lokal maupun instance Firestore jarak jauh. Modul fitur hanya perlu menyimpan dan membaca data aplikasi.

Untuk mencapai hal ini, modul fitur bergantung pada modul abstraksi, bukan implementasi database tertentu. Abstraksi ini menentukan API database aplikasi. Dengan kata lain, abstraksi ini menetapkan aturan tentang cara berinteraksi dengan database. Hal ini memungkinkan modul fitur untuk menggunakan database apa pun tanpa perlu mengetahui detail implementasi yang mendasarinya.

Modul implementasi konkret menyediakan implementasi aktual dari API yang ditentukan dalam modul abstraksi. Agar dapat melakukannya, modul implementasi juga bergantung pada modul abstraksi.

Injeksi dependensi

Saat ini Anda mungkin ingin tahu bagaimana modul fitur terhubung dengan modul implementasi. Jawabannya adalah Injeksi Dependensi. Modul fitur tidak langsung membuat instance database yang diperlukan. Sebaliknya, modul ini menentukan dependensi yang diperlukan. Dependensi tersebut kemudian disediakan secara eksternal, biasanya dalam modul aplikasi.

releaseImplementation(project(":database:impl:firestore"))

debugImplementation(project(":database:impl:room"))

androidTestImplementation(project(":database:impl:mock"))

Manfaat

Manfaat memisahkan API Anda dan implementasinya adalah sebagai berikut:

  • Dapat dipertukaran: Dengan memisahkan API dan modul implementasi secara jelas, Anda dapat mengembangkan beberapa implementasi untuk API yang sama, dan beralih di antara keduanya tanpa mengubah kode yang menggunakan API. Hal ini akan sangat bermanfaat dalam skenario saat Anda ingin memberikan kemampuan atau perilaku yang berbeda dalam konteks yang berbeda. Misalnya, implementasi tiruan untuk pengujian versus implementasi nyata untuk produksi.
  • Terpisah: Pemisahan berarti modul yang menggunakan abstraksi tidak bergantung pada teknologi tertentu. Jika Anda memilih untuk mengubah database dari Room ke Firestore nanti, prosesnya akan lebih mudah karena perubahan tersebut hanya akan terjadi pada modul tertentu yang melakukan tugas (modul implementasi) dan tidak akan memengaruhi modul lain yang menggunakan API database Anda.
  • Dapat diuji: Memisahkan API dari implementasinya akan sangat memudahkan pengujian. Anda dapat menulis kasus pengujian terhadap kontrak API. Anda juga dapat menggunakan berbagai implementasi untuk menguji beragam skenario dan kasus ekstrem, termasuk implementasi tiruan.
  • Meningkatkan performa build: Jika Anda memisahkan API dan implementasinya ke dalam modul yang berbeda, perubahan dalam modul implementasi tidak akan memaksa sistem build mengompilasi ulang modul, bergantung pada modul API. Hal ini menghasilkan waktu build yang lebih cepat dan peningkatan produktivitas, terutama dalam project besar yang waktu build-nya bisa sangat signifikan.

Kapan harus memisahkan

Memisahkan API Anda dari implementasinya akan sangat bermanfaat dalam kasus berikut:

  • Kemampuan beragam: Jika Anda dapat mengimplementasikan bagian-bagian sistem Anda dengan beberapa cara, API yang jelas memungkinkan pertukaran berbagai implementasi. Misalnya, Anda mungkin memiliki sistem rendering yang menggunakan OpenGL atau Vulkan, atau sistem penagihan yang berfungsi dengan Play atau API penagihan internal Anda.
  • Beberapa aplikasi: Jika Anda mengembangkan beberapa aplikasi dengan kemampuan bersama untuk berbagai platform, Anda dapat menentukan API umum dan mengembangkan implementasi yang spesifik per platform.
  • Tim independen: Dengan pemisahan, developer atau tim yang berbeda dapat mengerjakan bagian codebase yang berbeda secara bersamaan. Developer harus berfokus untuk memahami kontrak API dan menggunakannya dengan benar. Mereka tidak perlu mengkhawatirkan detail implementasi modul lain.
  • Codebase besar: Jika codebase berukuran besar atau kompleks, pemisahan API dari implementasi akan membuat kode lebih mudah dikelola. Tindakan ini memungkinkan Anda membagi codebase menjadi beberapa unit yang lebih terperinci, mudah dipahami, dan mudah dikelola.

Cara menerapkan

Untuk menerapkan inversi dependensi, ikuti langkah-langkah berikut:

  1. Buat modul abstraksi: Modul ini harus berisi API (antarmuka dan model) yang menentukan perilaku fitur Anda.
  2. Buat modul implementasi: Modul implementasi harus bergantung pada modul API dan menerapkan perilaku abstraksi.
    Modul tingkat tinggi tidak bergantung pada modul tingkat rendah secara langsung, sementara modul tingkat tinggi dan modul implementasi bergantung pada modul abstraksi.
    Gambar 10. Modul implementasi bergantung pada modul abstraksi.
  3. Buat modul tingkat tinggi yang bergantung pada modul abstraksi: Daripada bergantung secara langsung pada implementasi tertentu, buat modul Anda bergantung pada modul abstraksi. Modul tingkat tinggi tidak perlu mengetahui detail implementasi, tetapi hanya memerlukan kontrak (API).
    Modul tingkat tinggi bergantung pada abstraksi, bukan implementasi.
    Gambar 11. Modul tingkat tinggi bergantung pada abstraksi, bukan implementasi.
  4. Sediakan modul implementasi: Terakhir, Anda perlu menyediakan implementasi aktual untuk dependensi Anda. Implementasi spesifik bergantung pada konfigurasi project Anda, tetapi modul aplikasi biasanya adalah tempat yang tepat untuk melakukannya. Untuk menyediakan implementasi, tentukan implementasi tersebut sebagai dependensi untuk varian build yang dipilih atau set sumber pengujian.
    Modul aplikasi menyediakan implementasi aktual.
    Gambar 12. Modul aplikasi menyediakan implementasi aktual.

Praktik terbaik umum

Seperti yang disebutkan di awal, tidak ada cara yang tepat untuk mengembangkan aplikasi multi-modul. Sama seperti banyak arsitektur software, ada banyak cara untuk memodularisasi aplikasi. Meskipun demikian, rekomendasi umum berikut dapat membantu menjadikan kode Anda lebih mudah dibaca, mudah dikelola, dan dapat diuji.

Pastikan konfigurasi Anda konsisten

Setiap modul memunculkan overhead konfigurasi. Jika jumlah modul Anda mencapai batas tertentu, mengelola konfigurasi yang konsisten menjadi tantangan. Misalnya, modul harus menggunakan dependensi versi yang sama. Jika Anda perlu mengupdate sejumlah besar modul hanya untuk menambahkan versi dependensi, hal ini tidak hanya membutuhkan upaya, tetapi juga ruang untuk potensi kesalahan. Untuk mengatasi masalah ini, Anda dapat menggunakan salah satu alat gradle untuk memusatkan konfigurasi:

  • Katalog versi adalah daftar dependensi jenis yang aman yang dihasilkan oleh Gradle selama sinkronisasi. Ini adalah tempat utama untuk mendeklarasikan semua dependensi Anda dan tersedia untuk semua modul dalam project.
  • Gunakan plugin konvensi untuk membagikan logika build antar-modul.

Ekspos sesedikit mungkin

Antarmuka publik modul harus bersifat minimal dan hanya menampilkan hal-hal penting. Detail implementasi apa pun tidak boleh bocor ke luar. Buat cakupan semuanya sekecil mungkin. Gunakan cakupan visibilitas private atau internal Kotlin untuk membuat deklarasi bersifat pribadi. Saat mendeklarasikan dependensi dalam modul, pilih implementation daripada api. Yang kedua mengekspos dependensi transitif kepada konsumen modul Anda. Menggunakan implementasi dapat meningkatkan waktu build karena dapat mengurangi jumlah modul yang perlu dibuat ulang.

Memilih modul Kotlin & Java

Ada tiga jenis modul penting yang didukung Android Studio:

  • Modul aplikasi adalah titik entri ke aplikasi Anda. Aset dapat berisi kode sumber, resource, aset, dan AndroidManifest.xml. Output modul aplikasi adalah Android App Bundle (AAB) atau Paket Aplikasi Android (APK).
  • Modul library memiliki konten yang sama dengan modul aplikasi. Class ini digunakan oleh modul Android lain sebagai dependensi. Output modul library adalah Android Archive (AAR) yang secara struktural identik dengan modul aplikasi, tetapi dikompilasi menjadi file Android Archive (AAR) yang nantinya dapat digunakan oleh modul lain sebagai dependensi. Modul library memungkinkan enkapsulasi dan penggunaan kembali logika dan resource yang sama di banyak modul aplikasi.
  • Library Kotlin dan Java tidak berisi resource, aset, atau file manifes Android.

Karena modul Android memiliki overhead, sebaiknya Anda menggunakan Kotlin atau Java sebanyak mungkin.