โปรแกรมเมอร์แต่ละคนจะพัฒนาแอปบนอุปกรณ์เคลื่อนที่ตามวิสัยทัศน์ ซึ่งรวมถึงแนวคิดและมุมมองเกี่ยวกับวิธีการทำงานต่างๆ บางครั้งพวกเขาอาจเพิกเฉยต่อหลักการหลักของการเขียนโปรแกรมเชิงวัตถุหรือเชิงฟังก์ชัน ซึ่งอาจนำไปสู่การสับสนในหมู่นักพัฒนา
สิ่งนี้ไม่ดี - พวกเขาไม่สามารถจัดการกับรหัสของพวกเขาได้ และผู้พัฒนารายต่อไปที่ต้องการดูแลโครงการหรือแก้ไขโครงการอาจกลายเป็นเรื่องบ้า เป็นการดีกว่าที่จะสร้างโครงการดังกล่าวขึ้นมาใหม่ตั้งแต่ต้น เนื่องจากการบำรุงรักษากลายเป็นกระบวนการที่ซับซ้อน
จนกระทั่ง Google เปิดตัวสถาปัตยกรรมที่รองรับครั้งแรก บริษัทพัฒนาซอฟต์แวร์เกือบทุกแห่งใช้สถาปัตยกรรมของตัวเอง ซึ่งช่วยให้พวกเขาทำให้โค้ดชัดเจนขึ้นและทำให้สามารถสลับไปมาระหว่างโปรเจ็กต์ต่างๆ ได้ แต่ถ้านักพัฒนาเปลี่ยนบริษัท ก็ต้องใช้เวลาพอสมควรในการเรียนรู้สถาปัตยกรรมใหม่นั้นควบคู่ไปกับโครงการใหม่
ในขณะนี้ มีสถาปัตยกรรมที่แตกต่างกัน 16 แบบสำหรับนักพัฒนา Android ต้องขอบคุณ Google:
- 6 ตัวอย่างที่เสถียร (Java);
- ตัวอย่างเสถียร 2 ตัวอย่าง (Kotlin):
- ตัวอย่างภายนอก 4 ตัวอย่าง;
- ตัวอย่างที่เลิกใช้แล้ว 3 ตัวอย่าง
- อยู่ระหว่างการสุ่มตัวอย่าง 1 ตัวอย่าง
สถาปัตยกรรมใดก็ตามที่คุณใช้ขึ้นอยู่กับวัตถุประสงค์เฉพาะ วิธีการ และการประยุกต์ใช้ชุดเครื่องมือต่างๆ สำหรับการใช้งานฟังก์ชันต่างๆ และก็ขึ้นอยู่กับภาษาโปรแกรมด้วย
อย่างไรก็ตาม สถาปัตยกรรมเหล่านี้ทั้งหมดมีรากฐานทางสถาปัตยกรรมร่วมกันเพียงหนึ่งเดียวที่แบ่งตรรกะสำหรับการทำงานกับเครือข่าย ฐานข้อมูล การพึ่งพา และการประมวลผลการเรียกกลับเกือบเท่าๆ กัน
เครื่องมือที่ใช้ระหว่างกระบวนการ
หลังจากศึกษาสถาปัตยกรรมเหล่านี้ทั้งหมดแล้ว ฉันได้สร้างแนวทางที่เรียบง่ายขึ้นและได้สถาปัตยกรรมที่มีเลเยอร์น้อยลง ฉันจะแสดงวิธีใช้งานแอป Android แบบง่ายๆ ที่โหลดรายการข่าว ให้คุณบันทึกเรื่องราวลงในรายการโปรด แล้วลบออกหากจำเป็นโดยใช้วิธีการของฉัน
นี่คือบทสรุปของเทคโนโลยีที่ฉันใช้:
- คอตลิน เพื่อพัฒนาแอปควบคู่ไปกับ AndroidX ห้องสมุด
- ห้อง SQLite เป็นฐานข้อมูล
- สเตโธ เพื่อเรียกดูข้อมูลในฐาน
- ติดตั้งเพิ่ม2 พร้อมกับ RxJava2 เพื่อช่วยบันทึกคำขอของเซิร์ฟเวอร์และรับการตอบกลับของเซิร์ฟเวอร์
- ร่อน เพื่อประมวลผลภาพ
- ส่วนประกอบสถาปัตยกรรม Android (LiveData, ViewModel, Room) และ ReactiveX (RxJava2, RxKotlin и RxAndroid) สำหรับการสร้างการพึ่งพา การเปลี่ยนแปลงข้อมูลแบบไดนามิก และการประมวลผลแบบอะซิงโครนัส
นี่คือสแต็คเทคโนโลยีแอพมือถือที่ฉันใช้สำหรับโครงการของฉัน
มาเริ่มกันเลย
ก้าวแรก
เชื่อมต่อ AndroidX . ใน gradle.properties ที่ระดับแอป ให้เขียนดังนี้:
android.enableJetifier=true
android.useAndroidX=true
ตอนนี้จำเป็นต้องแทนที่การพึ่งพาใน build.gradle ที่ระดับโมดูลแอปจาก Android ถึง AndroidX คุณควรแยกการพึ่งพาทั้งหมดไปที่ ext, ดังที่คุณเห็นในตัวอย่างของ Kotlin เวอร์ชันสำเร็จรูปใน build.gradle ในระดับแอป จากนั้นฉันก็เพิ่มเวอร์ชัน Gradle ที่นั่น:
buildscript {
ext.kotlin_version = '1.3.0'
ext.gradle_version = '3.2.1'
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:$gradle_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
สำหรับการพึ่งพาอื่น ๆ ทั้งหมด ฉันจะสร้าง ext ที่ฉันเพิ่มการพึ่งพาทั้งหมดรวมถึงเวอร์ชัน SDK การแบ่งเวอร์ชันและการสร้างมวลการพึ่งพาซึ่งจะนำไปใช้เพิ่มเติมใน build.gradle ในระดับแอป จะมีลักษณะดังนี้:
ext {
compileSdkVersion = 28
minSdkVersion = 22
buildToolsVersion = '28.0.3'
targetSdkVersion = 28
appcompatVersion = '1.0.2'
supportVersion = '1.0.0'
supportLifecycleExtensionsVersion = '2.0.0'
constraintlayoutVersion = '1.1.3'
multiDexVersion = "2.0.0"
testJunitVersion = '4.12'
testRunnerVersion = '1.1.1'
testEspressoCoreVersion = '3.1.1'
testDependencies = [
junit : "junit:junit:$testJunitVersion",
runner : "androidx.test:runner:$testRunnerVersion",
espressoCore: "androidx.test.espresso:espresso-core:$testEspressoCoreVersion"
]
supportDependencies = [
kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version",
appCompat : "androidx.appcompat:appcompat:$appcompatVersion",
recyclerView : "androidx.recyclerview:recyclerview:$supportVersion",
design : "com.google.android.material:material:$supportVersion",
lifecycleExtension: "androidx.lifecycle:lifecycle-extensions:$supportLifecycleExtensionsVersion",
constraintlayout : "androidx.constraintlayout:constraintlayout:$constraintlayoutVersion",
multiDex : "androidx.multidex:multidex:$multiDexVersion"
]
}
มีการใช้ชื่อรุ่นและ Massif แบบสุ่ม หลังจากนั้น เราจะนำการพึ่งพาใน build.gradle ที่ระดับแอปดังนี้:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion rootProject.ext.compileSdkVersion as Integer
buildToolsVersion rootProject.ext.buildToolsVersion as String
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//Test
testImplementation testDependencies.junit
androidTestImplementation testDependencies.runner
androidTestImplementation testDependencies.espressoCore
//Support
implementation supportDependencies.kotlin
implementation supportDependencies.appCompat
implementation supportDependencies.recyclerView
implementation supportDependencies.design
implementation supportDependencies.lifecycleExtension
implementation supportDependencies.constraintlayout
implementation supportDependencies.multiDex
อย่าลืมระบุ multiDexEnabled true ในการกำหนดค่าเริ่มต้น ในกรณีส่วนใหญ่ คุณจะถึงขีดจำกัดจำนวนวิธีที่ใช้อย่างรวดเร็ว
ในทำนองเดียวกัน คุณต้องประกาศการพึ่งพาทั้งหมดของแอป มาเพิ่มการอนุญาตเพื่อเชื่อมต่อแอพของเรากับอินเทอร์เน็ต:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
หากไม่มีชื่อเพิ่มในรายการ คุณควรทำตั้งแต่ Stetho จะไม่เห็นแอปนิรนามและคุณจะไม่สามารถดูในฐานข้อมูลได้
การสร้างส่วนประกอบพื้นฐาน
เป็นที่น่าสังเกตว่ารูปแบบ MVVM (Model-View-ViewModel) ถูกใช้เป็นพื้นฐานสำหรับการสร้างสถาปัตยกรรมนี้
มาเริ่มการพัฒนากันเลย สิ่งแรกที่คุณต้องทำคือสร้างคลาสที่จะสืบทอด Application() ในชั้นเรียนนี้ เราจะให้สิทธิ์เข้าถึงบริบทของแอปเพื่อการใช้งานต่อไป
@SuppressWarnings("all")
class App : Application() {
companion object {
lateinit var instance: App
private set
}
override fun onCreate() {
super.onCreate()
instance = this
Stetho.initializeWithDefaults(this)
DatabaseCreator.createDatabase(this)
}
}
ขั้นตอนที่สองคือการสร้างส่วนประกอบแอพพื้นฐานที่เริ่มต้นด้วย ViewModel ซึ่งฉันจะใช้สำหรับแต่ละกิจกรรมหรือส่วนย่อย
abstract class BaseViewModel constructor(app: Application) : AndroidViewModel(app) {
override fun onCleared() {
super.onCleared()
}
}
แอพนี้ไม่มีฟังก์ชั่นที่ซับซ้อน แต่ใน ViewModel พื้นฐาน เราจะใส่ 3 LiveData หลัก :
- เกิดข้อผิดพลาดในการประมวลผล
- กำลังโหลดการประมวลผลโดยแสดงแถบความคืบหน้า
- และเนื่องจากฉันมีแอปที่มีรายการ กำลังประมวลผลใบเสร็จและความพร้อมของข้อมูลในอแด็ปเตอร์เป็นตัวยึดตำแหน่งที่จะแสดงในกรณีที่ไม่มีอยู่
val errorLiveData = MediatorLiveData<String>()
val isLoadingLiveData = MediatorLiveData<Boolean>()
val isEmptyDataPlaceholderLiveData = MediatorLiveData<Boolean>()
ในการถ่ายโอนผลลัพธ์ของการใช้งานฟังก์ชันไปยัง LiveData ฉันจะใช้ Consumer .
ในการประมวลผลข้อผิดพลาดในทุกที่ในแอป คุณต้องสร้าง Consumer ที่จะถ่ายโอน Throwable.message ค่าเป็น errorLiveData .
นอกจากนี้ ใน VewModel พื้นฐาน คุณจะต้องสร้างวิธีการที่จะได้รับรายการ LiveData เพื่อแสดงแถบความคืบหน้าระหว่างการใช้งาน
ViewModel พื้นฐานของเราจะมีลักษณะดังนี้:
abstract class BaseViewModel constructor(app: Application) : AndroidViewModel(app) {
val errorLiveData = MediatorLiveData<String>()
val isLoadingLiveData = MediatorLiveData<Boolean>()
val isEmptyDataPlaceholderLiveData = MediatorLiveData<Boolean>()
private var compositeDisposable: CompositeDisposable? = null
protected open val onErrorConsumer = Consumer<Throwable> {
errorLiveData.value = it.message
}
fun setLoadingLiveData(vararg mutableLiveData: MutableLiveData<*>) {
mutableLiveData.forEach { liveData ->
isLoadingLiveData.apply {
this.removeSource(liveData)
this.addSource(liveData) { this.value = false }
}
}
}
override fun onCleared() {
isLoadingLiveData.value = false
isEmptyDataPlaceholderLiveData.value = false
clearSubscription()
super.onCleared()
}
private fun clearSubscription() {
compositeDisposable?.apply {
if (!isDisposed) dispose()
compositeDisposable = null
}
}
}
ในแอพของเรา การสร้างกิจกรรมสองสามอย่างสำหรับสองหน้าจอนั้นไม่สมเหตุสมผลเลย (หน้าจอรายการข่าวและหน้าจอรายการโปรด) แต่เนื่องจากตัวอย่างนี้แสดงการใช้งานสถาปัตยกรรมที่เหมาะสมและขยายได้ง่าย ฉันจะสร้างแอปพื้นฐาน
แอพของเราจะสร้างขึ้นจาก 1 กิจกรรมและ 2 ส่วนที่เราจะขยายในกิจกรรมคอนเทนเนอร์ ไฟล์ XML ของกิจกรรมของเราจะเป็นดังนี้:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/flContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<include layout="@layout/include_placeholder"/>
<include layout="@layout/include_progress_bar" />
</FrameLayout>
โดยที่ รวม_placeholder และ include_progressbar จะมีลักษณะดังนี้:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:id="@+id/flProgress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_black_40">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@color/transparent" />
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:id="@+id/flPlaceholder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_transparent">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@color/transparent"
android:src="@drawable/ic_business_light_blue_800_24dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="40dp"
android:text="@string/empty_data"
android:textColor="@color/colorPrimary"
android:textStyle="bold" />
</FrameLayout>
BaseActivity ของเราจะมีลักษณะดังนี้:
abstract class BaseActivity<T : BaseViewModel> : AppCompatActivity(), BackPressedCallback,
ProgressViewCallback, EmptyDataPlaceholderCallback {
protected abstract val viewModelClass: Class<T>
protected abstract val layoutId: Int
protected abstract val containerId: Int
protected open val viewModel: T by lazy(LazyThreadSafetyMode.NONE) { ViewModelProviders.of(this).get(viewModelClass) }
protected abstract fun observeLiveData(viewModel: T)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(layoutId)
startObserveLiveData()
}
private fun startObserveLiveData() {
observeLiveData(viewModel)
}
}
มาปรับใช้วิธีการแสดงข้อผิดพลาดที่เป็นไปได้ในกระบวนการของกิจกรรมในอนาคตทั้งหมด ฉันจะทำในรูปแบบของ Toast ธรรมดาเพื่อความเรียบง่าย
protected open fun processError(error: String) = Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
และส่งข้อความแสดงข้อผิดพลาดนี้ไปยังวิธีการแสดง:
protected open val errorObserver = Observer<String> { it?.let { processError(it) } }
ในกิจกรรมพื้นฐาน ฉันจะเริ่มติดตามการเปลี่ยนแปลงของ errorLiveData ค่าที่อยู่ใน View Model พื้นฐาน startObserveLiveData() วิธีการจะกลายพันธุ์ดังนี้:
private fun startObserveLiveData() {
observeLiveData(viewModel)
with(viewModel) {
errorLiveData.observe(this@BaseActivity, errorObserver)
}
}
ตอนนี้ใช้ onErrorConsumer ของ ViewModel พื้นฐานเป็น onError คุณจะเห็นข้อความเกี่ยวกับข้อผิดพลาดของวิธีการดำเนินการ
สร้างวิธีการที่ให้คุณแทนที่ Fragments ใน Activity ด้วยความสามารถในการเพิ่ม Back Stack
protected open fun replaceFragment(fragment: Fragment, needToAddToBackStack: Boolean = true) {
val name = fragment.javaClass.simpleName
with(supportFragmentManager.beginTransaction()) {
replace(containerId, fragment, name)
if (needToAddToBackStack) {
addToBackStack(name)
}
commit()
}
}
มาสร้างอินเทอร์เฟซสำหรับแสดงความคืบหน้าและตัวยึดตำแหน่งในแอปที่ต้องการ
interface EmptyDataPlaceholderCallback {
fun onShowPlaceholder()
fun onHidePlaceholder()
}
interface ProgressViewCallback {
fun onShowProgress()
fun onHideProgress()
}
นำไปใช้ในกิจกรรมพื้นฐาน ฉันสร้างฟังก์ชันของการตั้งค่า ID ให้กับแถบความคืบหน้าและตัวยึดตำแหน่ง และยังเริ่มต้น Views เหล่านี้ด้วย
protected open fun hasProgressBar(): Boolean = false
protected abstract fun progressBarId(): Int
protected abstract fun placeholderId(): Int
private var vProgress: View? = null
private var vPlaceholder: View? = null
override fun onShowProgress() {
vProgress?.visibility = View.VISIBLE
}
override fun onHideProgress() {
vProgress?.visibility = View.GONE
}
override fun onShowPlaceholder() {
vPlaceholder?.visibility = View.VISIBLE
}
override fun onHidePlaceholder() {
vPlaceholder?.visibility = View.INVISIBLE
}
public override fun onStop() {
super.onStop()
onHideProgress()
}
และสุดท้ายใน onCreate วิธีตั้งค่า ID สำหรับ View:
if (hasProgressBar()) {
vProgress = findViewById(progressBarId())
vProgress?.setOnClickListener(null)
}
vPlaceholder = findViewById(placeholderId())
startObserveLiveData()
ฉันได้สะกดการสร้าง ViewModel พื้นฐานและกิจกรรมพื้นฐานแล้ว Basic Fragment จะถูกสร้างขึ้นตามหลักการเดียวกัน
เมื่อคุณสร้างแต่ละหน้าจอแยกกัน หากคุณกำลังพิจารณาการขยายเพิ่มเติมและการเปลี่ยนแปลงที่เป็นไปได้ คุณจะต้องสร้าง Fragment แยกจากกันด้วย ViewModel
หมายเหตุ:ในกรณีที่สามารถรวม Fragment ไว้ในคลัสเตอร์เดียวได้ และตรรกะทางธุรกิจไม่ได้หมายความถึงความซับซ้อนมหาศาล Fragment หลายส่วนอาจใช้ ViewModel เดียว
การสลับระหว่าง Fragments เกิดขึ้นเนื่องจากอินเทอร์เฟซที่ใช้งานในกิจกรรม ในการทำเช่นนี้ Fragment แต่ละส่วนควรมี วัตถุที่แสดงร่วม{ } ด้วยวิธีการสร้างวัตถุ Fragment ที่มีความสามารถในการถ่ายโอนอาร์กิวเมนต์ไปยัง Bundle :
companion object {
fun newInstance() = FavoriteFragment().apply { arguments = Bundle() }
}
โซลูชันด้านสถาปัตยกรรม
เมื่อสร้างส่วนประกอบพื้นฐานแล้ว ก็ถึงเวลาที่ต้องเน้นที่สถาปัตยกรรม แผนผังจะดูเหมือนสถาปัตยกรรมที่สะอาดโดย Robert C. Martin หรือลุงบ๊อบที่มีชื่อเสียง แต่เนื่องจากฉันใช้ RxJava2 , ฉันกำจัด ขอบเขต อินเทอร์เฟซ (เพื่อให้มั่นใจว่า กฎการพึ่งพา การดำเนินการ) ให้เป็นไปตามมาตรฐาน สังเกตได้ และ สมาชิก .
นอกเหนือจากนี้ การใช้ RxJava2 เครื่องมือ ฉันได้รวมการแปลงข้อมูลเพื่อการทำงานที่ยืดหยุ่นมากขึ้นด้วย มันเกี่ยวข้องกับการทำงานกับการตอบสนองของเซิร์ฟเวอร์และกับฐานข้อมูล
นอกจากโมเดลหลักแล้ว ฉันจะสร้างโมเดลการตอบสนองของเซิร์ฟเวอร์และโมเดลตารางแยกสำหรับ ห้อง . การแปลงข้อมูลระหว่างสองโมเดลนี้ คุณสามารถทำการเปลี่ยนแปลงใดๆ ในระหว่างกระบวนการแปลง แปลงการตอบสนองของเซิร์ฟเวอร์ และบันทึกข้อมูลที่จำเป็นลงในฐานก่อนที่จะแสดงบน UI และอื่นๆ
ส่วนย่อยมีหน้าที่รับผิดชอบ UI และ ViewModel Fragments มีหน้าที่รับผิดชอบในการดำเนินการตามตรรกะทางธุรกิจ หากตรรกะทางธุรกิจเกี่ยวข้องกับกิจกรรมทั้งหมด แสดงว่ากิจกรรมของ ViewModel
ViewModels รับข้อมูลจากผู้ให้บริการโดยการเริ่มต้นผ่าน val … โดย lazy{}, หากคุณต้องการวัตถุที่ไม่เปลี่ยนแปลง หรือ lateinit var ถ้ากลับกัน หลังจากการดำเนินการของตรรกะทางธุรกิจ หากคุณต้องการถ่ายโอนข้อมูลเพื่อเปลี่ยน UI คุณสร้าง MutableLiveData . ใหม่ ใน ViewModel ที่คุณจะใช้ใน observeLiveData() วิธีการของ Fragment ของเรา
ฟังดูค่อนข้างง่าย การนำไปปฏิบัติก็ตรงไปตรงมาเช่นกัน
องค์ประกอบที่สำคัญของสถาปัตยกรรมของเราคือตัวแปลงข้อมูลโดยอิงจากการแปลงอย่างง่ายจากประเภทข้อมูลหนึ่งไปยังอีกประเภทหนึ่ง สำหรับการแปลง RxJava สตรีมข้อมูล SingleTransformer หรือ FlowableTransformer ใช้แล้วแต่ประเภท ในกรณีของแอปของเรา อินเทอร์เฟซและคลาสนามธรรมของตัวแปลงมีลักษณะดังนี้:
interface BaseDataConverter<IN, OUT> {
fun convertInToOut(inObject: IN): OUT
fun convertOutToIn(outObject: OUT): IN
fun convertListInToOut(inObjects: List<IN>?): List<OUT>?
fun convertListOutToIn(outObjects: List<OUT>?): List<IN>?
fun convertOUTtoINSingleTransformer(): SingleTransformer<IN?, OUT>
fun convertListINtoOUTSingleTransformer(): SingleTransformer<List<OUT>, List<IN>>
}
abstract class BaseDataConverterImpl<IN, OUT> : BaseDataConverter<IN, OUT> {
override fun convertInToOut(inObject: IN): OUT = processConvertInToOut(inObject)
override fun convertOutToIn(outObject: OUT): IN = processConvertOutToIn(outObject)
override fun convertListInToOut(inObjects: List<IN>?): List<OUT> =
inObjects?.map { convertInToOut(it) } ?: listOf()
override fun convertListOutToIn(outObjects: List<OUT>?): List<IN> =
outObjects?.map { convertOutToIn(it) } ?: listOf()
override fun convertOUTtoINSingleTransformer() =
SingleTransformer<IN?, OUT> { it.map { convertInToOut(it) } }
override fun convertListINtoOUTSingleTransformer() =
SingleTransformer<List<OUT>, List<IN>> { it.map { convertListOutToIn(it) } }
protected abstract fun processConvertInToOut(inObject: IN): OUT
protected abstract fun processConvertOutToIn(outObject: OUT): IN
}
ในตัวอย่างนี้ ฉันใช้การแปลงพื้นฐาน เช่น โมเดล-โมเดล รายการโมเดล - รายการโมเดล และชุดค่าผสมเดียวกันแต่ใช้เฉพาะ SingleTransformer สำหรับการประมวลผลการตอบสนองของเซิร์ฟเวอร์และคำขอในฐานข้อมูล
เริ่มต้นด้วยเครือข่าย - ด้วย RestClient retrofitBuilder วิธีการจะเป็นดังนี้:
fun retrofitBuilder(): Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(NullOrEmptyConverterFactory().converterFactory())
.addConverterFactory(GsonConverterFactory.create(createGsonBuilder()))
.client(createHttpClient())
.build()
//base url
const val BASE_URL = "https://newsapi.org"
เมื่อใช้ API ของบริษัทอื่น มีโอกาสเสมอที่จะได้รับการตอบสนองที่เป็นโมฆะจากเซิร์ฟเวอร์ และอาจมีหลายสาเหตุ นั่นคือเหตุผลที่เพิ่ม NullOrEmptyConverterFactory จะช่วยจัดการกับสถานการณ์ นี่คือลักษณะ:
class NullOrEmptyConverterFactory : Converter.Factory() {
fun converterFactory() = this
override fun responseBodyConverter(type: Type?,
annotations: Array<Annotation>,
retrofit: Retrofit): Converter<ResponseBody, Any>? {
return Converter { responseBody ->
if (responseBody.contentLength() == 0L) {
null
} else {
type?.let {
retrofit.nextResponseBodyConverter<Any>(this, it, annotations)?.convert(responseBody) }
}
}
}
}
ในการสร้างแบบจำลอง จำเป็นต้องสร้างบน API ตัวอย่างเช่น ฉันจะใช้ APU ฟรีสำหรับการใช้งานที่ไม่ใช่เชิงพาณิชย์จาก newsapi.org มีรายการฟังก์ชันที่ร้องขอค่อนข้างกว้างขวาง แต่ฉันจะใช้ส่วนเล็ก ๆ สำหรับตัวอย่างนี้ หลังจากลงทะเบียนอย่างรวดเร็ว คุณจะสามารถเข้าถึง API และ รหัส API . ของคุณได้ ซึ่งจำเป็นสำหรับการร้องขอแต่ละครั้ง
เป็นปลายทาง ฉันจะใช้ https://newsapi.org/v2/everything . จาก แบบสอบถาม ที่แนะนำ ฉันเลือกสิ่งต่อไปนี้:q - คำค้นหา จาก - เรียงลำดับจากวันที่ ถึง - เรียงลำดับถึงวันที่ จัดเรียงตาม - จัดเรียงตามเกณฑ์ที่เลือก และต้องมี apiKey.
หลัง RestClient การสร้าง ฉันสร้างอินเทอร์เฟซ API ด้วย Query ที่เลือกสำหรับแอปของเรา:
interface NewsApi {
@GET(ENDPOINT_EVERYTHING)
fun getNews(@Query("q") searchFor: String?,
@Query("from") fromDate: String?,
@Query("to") toDate: String?,
@Query("sortBy") sortBy: String?,
@Query("apiKey") apiKey: String?): Single<NewsNetworkModel>
}
//endpoints
const val ENDPOINT_EVERYTHING = "/v2/everything"
เราจะได้รับคำตอบนี้ใน NewsNetworkModel:
data class NewsNetworkModel(@SerializedName("articles")
var articles: List<ArticlesNetworkModel>? = listOf())
data class ArticlesNetworkModel(@SerializedName("title")
var title: String? = null,
@SerializedName("description")
var description: String? = null,
@SerializedName("urlToImage")
var urlToImage: String? = null)
ข้อมูลเหล่านี้จากการตอบกลับทั้งหมดจะเพียงพอที่จะแสดงรายการพร้อมรูปภาพ ชื่อและคำอธิบายข่าว
สำหรับการนำแนวทางสถาปัตยกรรมของเราไปใช้ เรามาสร้างแบบจำลองทั่วไป:
interface News {
var articles: List<Article>?
}
class NewsModel(override var articles: List<Article>? = null) : News
interface Article {
var id: Long?
var title: String?
var description: String?
var urlToImage: String?
var isAddedToFavorite: Boolean?
var fragmentName: FragmentsNames?
}
class ArticleModel(override var id: Long? = null,
override var title: String? = null,
override var description: String? = null,
override var urlToImage: String? = null,
override var isAddedToFavorite: Boolean? = null,
override var fragmentName: FragmentsNames? = null) : Article
เนื่องจากโมเดล Article จะใช้สำหรับการเชื่อมต่อกับฐานข้อมูลและข้อมูลที่แสดงในอแด็ปเตอร์ เราจึงต้องเพิ่มระยะขอบ 2 อันที่ฉันจะใช้สำหรับเปลี่ยนองค์ประกอบ UI ในรายการ
เมื่อทุกอย่างพร้อมสำหรับคำขอ ฉันจะสร้างตัวแปลงสำหรับโมเดลเครือข่ายที่เราจะใช้ในการค้นหาข่าวสารที่ได้รับผ่าน NetworkModule
ตัวแปลงถูกสร้างขึ้นในลำดับย้อนกลับจากการซ้อน และแปลงตามลำดับโดยตรงตามลำดับ อันแรกที่ฉันสร้างใน Article อันที่สองใน News:
interface ArticlesBeanConverter
class ArticlesBeanDataConverterImpl : BaseDataConverterImpl<ArticlesNetworkModel, Article>(), ArticlesBeanConverter {
override fun processConvertInToOut(inObject: ArticlesNetworkModel): Article = inObject.run {
ArticleModel(null, title, description, urlToImage, false, FragmentsNames.NEWS)
}
override fun processConvertOutToIn(outObject: Article): ArticlesNetworkModel = outObject.run {
ArticlesNetworkModel(title, description, urlToImage)
}
}
interface NewsBeanConverter
class NewsBeanDataConverterImpl : BaseDataConverterImpl<NewsNetworkModel, News>(), NewsBeanConverter {
private val articlesConverter by lazy { ArticlesBeanDataConverterImpl() }
override fun processConvertInToOut(inObject: NewsNetworkModel): News = inObject.run {
NewsModel(articles?.let { articlesConverter.convertListInToOut(it) })
}
override fun processConvertOutToIn(outObject: News): NewsNetworkModel = outObject.run {
NewsNetworkModel(articles?.let { articlesConverter.convertListOutToIn(it) })
}
}
ดังที่คุณเห็นด้านบน ระหว่างการแปลงออบเจ็กต์ News การแปลงรายการออบเจ็กต์ Article จะถูกดำเนินการด้วย
เมื่อสร้างตัวแปลงสำหรับโมเดลเครือข่ายแล้ว ให้ดำเนินการสร้างโมดูล (เครือข่ายพื้นที่เก็บข้อมูล) ต่อไป เนื่องจากมักจะมี API อินเทอร์เฟซมากกว่า 1 หรือ 2 รายการ คุณต้องสร้าง BaseModule, API ที่พิมพ์, โมดูลเครือข่าย และ ConversionModel
นี่คือลักษณะ:
abstract class BaseNetworkModule<A, NM, M>(val api: A, val dataConverter: BaseDataConverter<NM, M>)
ดังนั้นจะเป็นสิ่งต่อไปนี้ใน NewsModule:
interface NewsModule {
fun getNews(fromDate: String? = null, toDate: String? = null, sortBy: String? = null): Single<News>
}
class NewsModuleImpl(api: NewsApi) : BaseNetworkModule<NewsApi, NewsNetworkModel, News>(api, NewsBeanDataConverterImpl()), NewsModule {
override fun getNews(fromDate: String?, toDate: String?, sortBy: String?): Single<News> =
api.getNews(searchFor = SEARCH_FOR, fromDate = fromDate, toDate = toDate, sortBy = sortBy, apiKey = API_KEY)
.compose(dataConverter.convertOUTtoINSingleTransformer())
.onErrorResumeNext(NetworkErrorUtils.rxParseError())
}
สำหรับ API นี้ คีย์ API เป็นพารามิเตอร์ที่สำคัญสำหรับการร้องขอโดยปลายทางที่แนะนำ นั่นคือเหตุผลที่คุณต้องตรวจสอบให้แน่ใจว่าไม่ได้ระบุพารามิเตอร์ทางเลือกไว้ล่วงหน้า และคุณจำเป็นต้องลบล้างพารามิเตอร์ดังกล่าวโดยค่าเริ่มต้น
ดังที่คุณเห็นด้านบน ฉันใช้การแปลงข้อมูลระหว่างการประมวลผลการตอบสนอง
มาทำงานกับฐานข้อมูลกันเถอะ ฉันสร้างฐานข้อมูลแอป เรียกมันว่า AppDatabase และสืบทอดจาก RoomDatabase() .
สำหรับการเริ่มต้นฐานข้อมูล จำเป็นต้องสร้าง DatabaseCreator ซึ่งควรเริ่มต้นใน แอป ชั้นเรียน
object DatabaseCreator {
lateinit var database: AppDatabase
private val isDatabaseCreated = MutableLiveData<Boolean>()
private val mInitializing = AtomicBoolean(true)
@SuppressWarnings("CheckResult")
fun createDatabase(context: Context) {
if (mInitializing.compareAndSet(true, false).not()) return
isDatabaseCreated.value = false
Completable.fromAction { database = Room.databaseBuilder(context, AppDatabase::class.java, DB_NAME).build() }
.compose { completableToMain(it) }
.subscribe({ isDatabaseCreated.value = true }, { it.printStackTrace() })
}
}
ตอนนี้อยู่ใน onCreate() วิธีการของ แอป คลาส I เริ่มต้น Steho และฐานข้อมูล:
override fun onCreate() {
super.onCreate()
instance = this
Stetho.initializeWithDefaults(this)
DatabaseCreator.createDatabase(this)
}
เมื่อฐานข้อมูลถูกสร้างขึ้น ฉันจะสร้าง Dao พื้นฐานด้วยเมธอด insert() เดียวภายใน:
@Dao
interface BaseDao<in I> {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(obj: I)
}
ตามแนวคิดของแอพของเรา ฉันจะบันทึกข่าวที่ฉันชอบ รับรายการบทความที่บันทึกไว้ ลบข่าวที่บันทึกไว้ด้วยรหัสของมัน หรือลบข่าวทั้งหมดออกจากตาราง NewsDao ของเรา จะเป็นดังนี้:
@Dao
interface NewsDao : BaseDao<NewsDatabase> {
@Query("SELECT * FROM $NEWS_TABLE")
fun getNews(): Single<List<NewsDatabase>>
@Query("DELETE FROM $NEWS_TABLE WHERE id = :id")
fun deleteNewsById(id: Long)
@Query("DELETE FROM $NEWS_TABLE")
fun deleteFavoriteNews()
}
และตารางข่าวจะเป็นดังนี้:
@Entity(tableName = NEWS_TABLE)
data class NewsDatabase(@PrimaryKey var id: Long?,
var title: String?,
var description: String?,
var urlToImage: String?)
เมื่อสร้างตารางแล้ว เรามาลิงก์กับฐานข้อมูลกัน:
@Database(entities = [NewsDatabase::class], version = DB_VERSION)
abstract class AppDatabase : RoomDatabase() {
abstract fun newsDao(): NewsDao
}
ตอนนี้ เราสามารถทำงานกับฐานข้อมูล บันทึก และดึงข้อมูลออกจากฐานข้อมูลได้
สำหรับโมดูล (เครือข่ายพื้นที่เก็บข้อมูล) ฉันจะสร้างตัวแปลงโมเดล - โมเดลตารางฐานข้อมูล:
interface NewsDatabaseConverter
class NewsDatabaseDataConverterImpl : BaseDataConverterImpl<Article, NewsDatabase>(), NewsDatabaseConverter {
override fun processConvertInToOut(inObject: Article): NewsDatabase =
inObject.run {
NewsDatabase(id, title, description, urlToImage)
}
override fun processConvertOutToIn(outObject: NewsDatabase): Article =
outObject.run {
ArticleModel(id, title, description, urlToImage, true, FragmentsNames.FAVORITES)
}
}
BaseRepository พร้อมใช้งานสำหรับการทำงานกับตารางต่างๆ มาเขียนกันเถอะ จะมีลักษณะดังนี้ในเวอร์ชันที่ง่ายที่สุดซึ่งเพียงพอสำหรับแอป:
abstract class BaseRepository<M, DBModel> {
protected abstract val dataConverter: BaseDataConverter<M, DBModel>
protected abstract val dao: BaseDao<DBModel>
}
หลังจากสร้าง BaseRepository ฉันจะสร้าง NewsRepository :
interface NewsRepository {
fun saveNew(article: Article): Single<Article>
fun getSavedNews(): Single<List<Article>>
fun deleteNewsById(id: Long): Single<Unit>
fun deleteAll(): Single<Unit>
}
object NewsRepositoryImpl : BaseRepository<Article, NewsDatabase>(), NewsRepository {
override val dataConverter by lazy { NewsDatabaseDataConverterImpl() }
override val dao by lazy { DatabaseCreator.database.newsDao() }
override fun saveNew(article: Article): Single<Article> =
Single.just(article)
.map { dao.insert(dataConverter.convertInToOut(it)) }
.map { article }
override fun getSavedNews(): Single<List<Article>> =
dao.getNews().compose(dataConverter.convertListINtoOUTSingleTransformer())
override fun deleteNewsById(id: Long): Single<Unit> =
Single.just(dao.deleteNewsById(id))
override fun deleteAll(): Single<Unit> =
Single.just(dao.deleteFavoriteNews())
}
เมื่อมีการสร้างที่เก็บถาวรและโมดูล ข้อมูลควรไหลจากผู้ให้บริการแอพที่จะขอข้อมูลจากเครือข่ายหรือฐานข้อมูลขึ้นอยู่กับข้อกำหนด ผู้ให้บริการควรรวมทั้งสองที่เก็บ เมื่อพิจารณาถึงความสามารถของโมเดลและที่เก็บที่หลากหลาย ฉันจะสร้าง BaseProvider:
abstract class BaseProvider<NM, DBR> {
val repository: DBR = this.initRepository()
val networkModule: NM = this.initNetworkModule()
protected abstract fun initRepository(): DBR
protected abstract fun initNetworkModule(): NM
}
แล้วก็ NewsProvider จะมีลักษณะดังนี้:
interface NewsProvider {
fun loadNewsFromServer(fromDate: String? = null, toDate: String? = null, sortBy: String? = null): Single<News>
fun saveNewToDB(article: Article): Single<Article>
fun getSavedNewsFromDB(): Single<List<Article>>
fun deleteNewsByIdFromDB(id: Long): Single<Unit>
fun deleteNewsFromDB(): Single<Unit>
}
object NewsProviderImpl : BaseProvider<NewsModule, NewsRepositoryImpl>(), NewsProvider {
override fun initRepository() = NewsRepositoryImpl
override fun initNetworkModule() = NewsModuleImpl(RestClient.retrofitBuilder().create(NewsApi::class.java))
override fun loadNewsFromServer(fromDate: String?, toDate: String?, sortBy: String?) = networkModule.getNews(fromDate, toDate, sortBy)
override fun saveNewToDB(article: Article) = repository.saveNew(article)
override fun getSavedNewsFromDB() = repository.getSavedNews()
override fun deleteNewsByIdFromDB(id: Long) = repository.deleteNewsById(id)
override fun deleteNewsFromDB() = repository.deleteAll()
}
ตอนนี้เราจะได้รับรายการข่าวอย่างง่ายดาย ใน NewsViewModel เราจะประกาศวิธีการทั้งหมดของผู้ให้บริการของเราสำหรับการใช้งานต่อไป:
val loadNewsSuccessLiveData = MutableLiveData<News>()
val loadLikedNewsSuccessLiveData = MutableLiveData<List<Article>>()
val deleteLikedNewsSuccessLiveData = MutableLiveData<Boolean>()
private val loadNewsSuccessConsumer = Consumer<News> { loadNewsSuccessLiveData.value = it }
private val loadLikedNewsSuccessConsumer = Consumer<List<Article>> { loadLikedNewsSuccessLiveData.value = it }
private val deleteLikedNewsSuccessConsumer = Consumer<Unit> { deleteLikedNewsSuccessLiveData.value = true }
private val dataProvider by lazy { NewsProviderImpl }
init {
isLoadingLiveData.apply { addSource(loadNewsSuccessLiveData) { value = false } }
@SuppressLint("CheckResult")
fun loadNews(fromDate: String? = null, toDate: String? = null, sortBy: String? = null) {
isLoadingLiveData.value = true
isEmptyDataPlaceholderLiveData.value = false
dataProvider.loadNewsFromServer(fromDate, toDate, sortBy)
.compose(RxUtils.ioToMainTransformer())
.subscribe(loadNewsSuccessConsumer, onErrorConsumer)
}
@SuppressLint("CheckResult")
fun saveLikedNew(article: Article) {
Single.fromCallable { Unit }
.flatMap { dataProvider.saveNewToDB(article) }
.compose(RxUtils.ioToMainTransformerSingle())
.subscribe({}, { onErrorConsumer })
}
@SuppressLint("CheckResult")
fun removeLikedNew(id: Long) {
Single.fromCallable { Unit }
.flatMap { dataProvider.deleteNewsByIdFromDB(id) }
.compose(RxUtils.ioToMainTransformerSingle())
.subscribe({}, { onErrorConsumer })
}
@SuppressLint("CheckResult")
fun loadLikedNews() {
Single.fromCallable { Unit }
.flatMap { dataProvider.getSavedNewsFromDB() }
.compose(RxUtils.ioToMainTransformerSingle())
.subscribe(loadLikedNewsSuccessConsumer, onErrorConsumer)
}
@SuppressLint("CheckResult")
fun removeLikedNews() {
Single.fromCallable { Unit }
.flatMap { dataProvider.deleteNewsFromDB() }
.compose(RxUtils.ioToMainTransformerSingle())
.subscribe(deleteLikedNewsSuccessConsumer, onErrorConsumer)
}
หลังจากประกาศวิธีการทั้งหมดที่ดำเนินการตามตรรกะทางธุรกิจใน ViewModel แล้ว เราจะโทรกลับจาก Fragment ซึ่งอยู่ใน observeLiveData() ผลลัพธ์ของแต่ละ LiveData . ที่ประกาศ จะถูกดำเนินการ
เพื่อนำไปใช้อย่างง่ายดายใน SEARCH_FOR พารามิเตอร์ที่ฉันสุ่มเลือก Apple และการเรียงลำดับเพิ่มเติมจะดำเนินการโดย ความนิยม แท็ก หากจำเป็น คุณสามารถเพิ่มฟังก์ชันขั้นต่ำสำหรับการเปลี่ยนพารามิเตอร์เหล่านี้ได้
เนื่องจาก newsapi.org ไม่ได้ให้รหัสข่าวแก่คุณ ฉันจึงยอมรับดัชนีองค์ประกอบเป็นรหัส การจัดเรียงตามแท็กความนิยมยังใช้งานผ่าน API แต่เพื่อหลีกเลี่ยงการเขียนข้อมูลใหม่ด้วยรหัสเดียวกันในฐานระหว่างการเรียงลำดับตามความนิยม ฉันจะตรวจสอบความพร้อมใช้งานของข้อมูลในฐานก่อนที่จะโหลดรายการข่าว หากฐานว่างเปล่า - กำลังโหลดรายการใหม่ หากไม่ - การแจ้งเตือนจะปรากฏขึ้น
มาเรียกใน onViewCreated() วิธีการของ NewsFragment วิธีการดังต่อไปนี้:
private fun loadLikedNews() {
viewModel.loadLikedNews()
}
เนื่องจากฐานของเราว่างเปล่า เมธอด loadNews() จะเปิดตัว ใน observeLiveData วิธี ฉันจะใช้การโหลด LiveData - viewModel.loadNewsSuccessLiveData.observe(..){news →}, ซึ่งเราจะได้รับรายชื่อบทความข่าวหากคำขอสำเร็จแล้วโอนไปยังอะแดปเตอร์:
isEmptyDataPlaceholderLiveData.value = news.articles?.isEmpty()
with(newsAdapter) {
news.articles?.toMutableList()?.let {
clear()
addAll(it)
}
notifyDataSetChanged()
}
loadNewsSuccessLiveData.value = null
เมื่อเปิดแอปแล้ว คุณจะเห็นผลลัพธ์ดังต่อไปนี้:
ในเมนูแถบเครื่องมือทางด้านขวา คุณจะเห็น 2 ตัวเลือก - การเรียงลำดับและรายการโปรด มาจัดเรียงรายการตามความนิยมและได้ผลลัพธ์ดังต่อไปนี้:
หากคุณไปที่รายการโปรด คุณจะเห็นตัวยึดตำแหน่งเท่านั้น เนื่องจากไม่มีข้อมูลในฐาน หน้าจอรายการโปรดจะมีลักษณะดังนี้:
ส่วน UI ของรายการโปรดมีหน้าจอสำหรับแสดงรายการข่าวที่ชอบและมีเพียงตัวเลือกเดียวในแถบเครื่องมือสำหรับการล้างฐานข้อมูล เมื่อคุณบันทึกข้อมูลโดยคลิกที่ “ถูกใจ” หน้าจอจะมีลักษณะดังนี้:
ตามที่ฉันเขียนไว้ข้างต้น ในโมเดลมาตรฐาน 2 ระยะขอบเพิ่มเติมถูกเพิ่มไปยังโมเดลทั่วไป และระยะขอบเหล่านี้ใช้สำหรับข้อมูลที่แสดงในอแด็ปเตอร์ ตอนนี้คุณสามารถเห็นว่าองค์ประกอบของรายการข่าวที่บันทึกไว้ไม่มีตัวเลือกในการเพิ่มในรายการโปรด
var isAddedToFavorite: Boolean?
var fragmentName: FragmentsNames?
หากคุณคลิก "ถูกใจ" อีกครั้ง องค์ประกอบที่บันทึกไว้จะถูกลบออกจากฐาน
สรุปผล
ดังนั้นฉันจึงแสดงให้คุณเห็นถึงแนวทางที่เรียบง่ายและชัดเจนในการพัฒนาแอพ Android เรายังคงรักษาหลักการสำคัญของ Clean Architecture แต่ลดความซับซ้อนให้มากที่สุด
อะไรคือความแตกต่างระหว่างสถาปัตยกรรมที่ฉันให้คุณกับสถาปัตยกรรมที่สะอาดจากคุณมาร์ติน? ในตอนแรก ฉันสังเกตว่าสถาปัตยกรรมของฉันคล้ายกับ CA เนื่องจากใช้เป็นพื้นฐาน นี่คือแผน CA ด้านล่าง:
เหตุการณ์ไปที่ Presenter จากนั้นไปที่ ใช้กรณีและปัญหา ใช้กรณี ร้องขอ ที่เก็บ Repository รับข้อมูล สร้าง Entity, และโอนไปยัง UseCase ดังนั้น กรณีการใช้งาน รับเอนทิตีที่จำเป็นทั้งหมด หลังจากใช้ตรรกะทางธุรกิจแล้ว คุณจะได้ผลลัพธ์ที่กลับมาที่ พรีเซ็นเตอร์ และในที่สุดก็โอนผลลัพธ์ไปที่ UI
ในรูปแบบด้านล่าง ตัวควบคุม วิธีการเรียกจาก InputPort ที่ใช้ UseCase และ OutputPort อินเทอร์เฟซได้รับการตอบกลับนี้และ ผู้นำเสนอ ดำเนินการมัน แทนที่จะเป็น UseCase ขึ้นอยู่กับ พรีเซ็นเตอร์ ขึ้นอยู่กับอินเทอร์เฟซในเลเยอร์ และไม่ขัดแย้งกับ กฎการพึ่งพา และผู้นำเสนอควรใช้อินเทอร์เฟซนี้
ดังนั้น กระบวนการที่ดำเนินการในเลเยอร์ภายนอกจะไม่ส่งผลต่อกระบวนการในเลเยอร์ภายใน Entity ในสถาปัตยกรรมที่สะอาดคืออะไร? อันที่จริงแล้ว มันคือทุกอย่างที่ไม่ได้ขึ้นอยู่กับแอพเฉพาะ และมันจะเป็นแนวคิดทั่วไปสำหรับแอพจำนวนมาก แต่ในกระบวนการพัฒนาอุปกรณ์เคลื่อนที่ เอนทิตีเป็นวัตถุทางธุรกิจของแอป ซึ่งมีกฎทั่วไปและระดับสูง (ตรรกะทางธุรกิจของแอป)
แล้วเกตเวย์ล่ะ อย่างที่ฉันเห็น เกตเวย์ เป็นที่เก็บข้อมูลสำหรับการทำงานกับฐานข้อมูลและโมดูลสำหรับการทำงานกับเครือข่าย เรากำจัดคอนโทรลเลอร์ออกไปตั้งแต่เริ่มแรก Clean Architecture ถูกสร้างขึ้นสำหรับการจัดโครงสร้างแอปทางธุรกิจที่มีความซับซ้อนสูง และตัวแปลงข้อมูลก็ทำหน้าที่ของมันในแอปของฉัน ViewModels ถ่ายโอนข้อมูลไปยัง Fragments สำหรับการประมวลผล UI แทนที่ Presenters
ในแนวทางของฉัน ฉันยังปฏิบัติตามกฎการพึ่งพาอย่างเคร่งครัด และตรรกะของที่เก็บ โมดูล โมเดล และผู้ให้บริการถูกห่อหุ้มไว้ และเข้าถึงสิ่งเหล่านี้ได้ผ่านอินเทอร์เฟซ ดังนั้น การเปลี่ยนแปลงของเลเยอร์ภายนอกจะไม่ส่งผลต่อเลเยอร์ภายใน และขั้นตอนการดำเนินการโดยใช้ RxJava2 , KotlinRx และ Kotlin LiveData ทำให้งานของนักพัฒนาซอฟต์แวร์ง่ายขึ้น ชัดเจนขึ้น และโค้ดอ่านได้ดีและขยายได้ง่าย