Computer >> Máy Tính >  >> Hệ thống >> Android

Cách đơn giản hóa kiến ​​trúc ứng dụng Android của bạn:Hướng dẫn chi tiết với các mẫu mã

Các lập trình viên cá nhân phát triển ứng dụng di động của họ theo tầm nhìn của họ, bao gồm cả ý tưởng và quan điểm của họ về cách thực hiện các tác vụ khác nhau. Đôi khi họ có thể bỏ qua các nguyên tắc chính của lập trình hướng đối tượng hoặc lập trình chức năng, điều này có thể dẫn đến sự mất phương hướng giữa các nhà phát triển.

Điều này thật tệ - họ sẽ không thể xử lý mã của mình. Và nhà phát triển tiếp theo, những người cần duy trì dự án hoặc sửa đổi nó có thể trở nên điên rồ. Tốt hơn là nên xây dựng lại các dự án như vậy từ đầu, vì bảo trì trở thành một quá trình phức tạp.

Cho đến khi Google phát hành kiến ​​trúc được hỗ trợ đầu tiên, hầu hết mọi công ty phát triển phần mềm đều sử dụng kiến ​​trúc của riêng mình. Điều này đã giúp họ làm cho mã của họ rõ ràng hơn và có thể chuyển đổi giữa các dự án. Nhưng nếu một nhà phát triển thay đổi công ty, họ sẽ mất một khoảng thời gian để tìm hiểu kiến ​​trúc mới đó cùng với một dự án mới.

Hiện tại, có 16 kiến ​​trúc khác nhau dành cho các nhà phát triển Android, nhờ Google:

  • 6 mẫu ổn định (Java);
  • 2 mẫu ổn định (Kotlin):
  • 4 mẫu bên ngoài;
  • 3 mẫu không dùng nữa;
  • 1 mẫu đang được xử lý.

Việc bạn sử dụng kiến ​​trúc nào phụ thuộc vào mục đích, cách tiếp cận cụ thể của bạn và việc áp dụng các bộ công cụ khác nhau để triển khai các chức năng khác nhau. Và nó phụ thuộc vào ngôn ngữ lập trình.

Tuy nhiên, tất cả các kiến ​​trúc này đều có một nền tảng kiến ​​trúc chung phân chia logic gần như bằng nhau để làm việc với mạng, cơ sở dữ liệu, phụ thuộc và xử lý lệnh gọi lại.

Các công cụ được sử dụng trong quá trình

Sau khi nghiên cứu tất cả các kiến ​​trúc này, tôi đã xây dựng một cách tiếp cận đơn giản hóa và đưa ra một kiến ​​trúc có ít lớp hơn. Tôi sẽ chỉ cho bạn cách triển khai một ứng dụng Android đơn giản tải danh sách tin tức, cho phép bạn lưu các câu chuyện vào Mục yêu thích và sau đó xóa nếu cần bằng cách sử dụng phương pháp của tôi.

Cách đơn giản hóa kiến ​​trúc ứng dụng Android của bạn:Hướng dẫn chi tiết với các mẫu mã

Đây là bản tóm tắt về công nghệ tôi đã sử dụng:

  • Kotlin để phát triển ứng dụng cùng với AndroidX thư viện
  • Phòng SQLite làm cơ sở dữ liệu
  • Stetho để duyệt dữ liệu trong căn cứ
  • Trang bị thêm2 cùng với RxJava2 để giúp ghi lại các yêu cầu của máy chủ và nhận được phản hồi của máy chủ.
  • Lướt nhẹ để xử lý hình ảnh
  • Thành phần kiến ​​trúc Android (LiveData, ViewModel, Room) và ReactiveX (RxJava2, RxKotlin и RxAndroid) để xây dựng các phần phụ thuộc, thay đổi dữ liệu động và xử lý không đồng bộ.

Vì vậy, đây là ngăn xếp công nghệ ứng dụng dành cho thiết bị di động mà tôi đã sử dụng cho dự án của mình.

Hãy bắt đầu

Các bước đầu tiên

Kết nối AndroidX . Trong gradle.properties ở cấp ứng dụng, hãy viết như sau:

android.enableJetifier=true
android.useAndroidX=true

Bây giờ, cần phải thay thế các phụ thuộc trong build.gradle ở cấp mô-đun ứng dụng từ Android đến AndroidX. Bạn nên trích xuất tất cả các phần phụ thuộc vào ext, như bạn có thể thấy trong ví dụ về lập phiên bản Kotlin out-of-the-box trong build.gradle ở cấp ứng dụng. Và sau đó tôi thêm phiên bản Gradle vào đó:

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
    }
}

Đối với tất cả các phần phụ thuộc khác, tôi sẽ xây dựng phần mở rộng của nó tệp, nơi tôi thêm hoàn toàn tất cả các phụ thuộc bao gồm các phiên bản SDK, phân chia lập phiên bản và tạo khối lượng phụ thuộc sẽ được triển khai thêm trong build.gradle ở cấp ứng dụng. Nó sẽ giống như sau:

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"
    ]
}

Phiên bản và tên khối lượng được thực hiện ngẫu nhiên. Sau đó, chúng tôi sẽ triển khai các phần phụ thuộc trong build.gradle ở cấp ứng dụng như sau:

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

Đừng quên chỉ định multiDexEnabled true trong cấu hình mặc định. Trong hầu hết các trường hợp, bạn sẽ nhanh chóng đạt đến giới hạn số lượng phương pháp được sử dụng.

Theo cách tương tự, bạn cần khai báo tất cả các phụ thuộc của ứng dụng. Hãy thêm quyền để kết nối ứng dụng của chúng tôi với Internet:

 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />

Nếu không có tên nào được thêm vào tệp kê khai, bạn nên làm điều đó kể từ Stetho sẽ không thấy ứng dụng không tên và bạn sẽ không thể xem xét cơ sở dữ liệu.

Xây dựng các thành phần cơ bản

Cần lưu ý rằng mẫu MVVM (Model-View-ViewModel) đã được sử dụng làm cơ sở để xây dựng kiến ​​trúc này.

Hãy bắt đầu phát triển. Điều đầu tiên bạn cần làm là tạo một lớp sẽ kế thừa Application (). Trong lớp này, chúng tôi sẽ cấp quyền truy cập vào ngữ cảnh ứng dụng để sử dụng thêm.

@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)
    }
}

Bước thứ hai là tạo các thành phần ứng dụng cơ bản bắt đầu với ViewModel, mà tôi sẽ sử dụng cho từng Hoạt động hoặc Phân đoạn.

abstract class BaseViewModel constructor(app: Application) : AndroidViewModel(app) {

    override fun onCleared() {
        super.onCleared()
    }
}

Ứng dụng này không có chức năng phức tạp. Nhưng trong ViewModel cơ bản, chúng tôi sẽ đặt 3 LiveData chính :

  • xử lý lỗi
  • đang tải xử lý với thanh tiến trình được hiển thị
  • và vì tôi có một ứng dụng với các danh sách, nên việc xử lý biên nhận và dữ liệu sẵn có trong bộ điều hợp như một trình giữ chỗ hiển thị khi chúng vắng mặt.
val errorLiveData = MediatorLiveData<String>()
    val isLoadingLiveData = MediatorLiveData<Boolean>()
    val isEmptyDataPlaceholderLiveData = MediatorLiveData<Boolean>()

Để chuyển kết quả của việc triển khai chức năng sang LiveData, tôi sẽ sử dụng Người tiêu dùng .

Để xử lý lỗi ở bất kỳ vị trí nào trong ứng dụng, bạn cần tạo Người tiêu dùng sẽ chuyển Throwable.message giá trị thành errorLiveData .

Ngoài ra, trong VewModel cơ bản, bạn sẽ cần tạo một phương thức nhận danh sách LiveData để hiển thị thanh tiến trình trong quá trình triển khai chúng.

ViewModel cơ bản của chúng ta sẽ trông như thế này:

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
        }
    }
}


Trong ứng dụng của chúng tôi, không hợp lý khi tạo một vài Hoạt động cho hai màn hình (màn hình danh sách tin tức và màn hình danh sách yêu thích). Nhưng vì mẫu này cho thấy việc triển khai kiến ​​trúc tối ưu và có thể mở rộng dễ dàng, tôi sẽ tạo một ứng dụng cơ bản.

Ứng dụng của chúng tôi sẽ được xây dựng dựa trên 1 Hoạt động và 2 Phân đoạn mà chúng tôi sẽ tăng cường trong hoạt động Vùng chứa. Tệp XML của Hoạt động của chúng tôi sẽ như sau:

<?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>

nơi include_placeholder include_progressbar sẽ trông như thế này:

<?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 của chúng tôi sẽ giống như sau:

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)
    }
}

Hãy triển khai cách hiển thị các lỗi có thể xảy ra trong quy trình của tất cả các Hoạt động trong tương lai. Mình sẽ làm dưới dạng Toast thông thường cho đơn giản.

protected open fun processError(error: String) = Toast.makeText(this, error, Toast.LENGTH_SHORT).show()

và gửi văn bản lỗi này đến phương thức hiển thị:

protected open val errorObserver = Observer<String> { it?.let { processError(it) } }

Trong Hoạt động cơ bản, tôi sẽ bắt đầu cập nhật các thay đổi của errorLiveData giá trị nằm trong Mô hình xem cơ bản. startObserveLiveData () phương thức sẽ thay đổi như sau:

private fun startObserveLiveData() {
        observeLiveData(viewModel)
        with(viewModel) {
            errorLiveData.observe(this@BaseActivity, errorObserver)
        }
    }

Hiện đang sử dụng onErrorConsumer của ViewModel cơ bản dưới dạng onError bộ xử lý, bạn sẽ thấy thông báo về lỗi phương pháp đã triển khai.

Tạo một phương thức cho phép bạn thay thế các Fragment trong Activity bằng khả năng thêm vào 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()
        }
    }

Hãy tạo giao diện để hiển thị tiến trình và trình giữ chỗ trong các vị trí ứng dụng cần thiết.

interface EmptyDataPlaceholderCallback {

    fun onShowPlaceholder()

    fun onHidePlaceholder()
}
interface ProgressViewCallback {

    fun onShowProgress()

    fun onHideProgress()
}

Thực hiện chúng trong Hoạt động cơ bản. Tôi đã tạo các chức năng của cài đặt ID cho thanh tiến trình và trình giữ chỗ, đồng thời khởi tạo các Chế độ xem này.

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()
    }

Và cuối cùng trong onCreate phương pháp tôi đặt ID cho Chế độ xem:

if (hasProgressBar()) {
            vProgress = findViewById(progressBarId())
            vProgress?.setOnClickListener(null)
        }
        vPlaceholder = findViewById(placeholderId())
        startObserveLiveData()

Tôi đã viết chính tả việc tạo ViewModel cơ bản và Hoạt động cơ bản. Phân đoạn cơ bản sẽ được tạo theo nguyên tắc tương tự.

Khi bạn tạo từng màn hình riêng biệt, nếu bạn đang xem xét thêm phần mở rộng và các thay đổi có thể xảy ra, bạn cần tạo một Fragment riêng với ViewModel của nó.

Lưu ý:trong trường hợp các Phân đoạn có thể được kết hợp trong một cụm và logic nghiệp vụ không ngụ ý độ phức tạp lớn, một số Phân đoạn có thể sử dụng một ViewModel.

Việc chuyển đổi giữa các Phân đoạn xảy ra do các giao diện được triển khai trong Hoạt động. Để thực hiện việc này, mỗi Fragment phải có một đối tượng đồng hành {} với phương pháp xây dựng đối tượng Fragment với khả năng chuyển các đối số đến Bundle :

companion object {
        fun newInstance() = FavoriteFragment().apply { arguments = Bundle() }
    }

Giải pháp kiến ​​trúc

Khi các thành phần cơ bản được tạo ra, đó là lúc tập trung vào kiến ​​trúc. Về mặt sơ đồ, nó sẽ giống như kiến ​​trúc sạch sẽ của Robert C. Martin hoặc Uncle Bob nổi tiếng. Nhưng vì tôi sử dụng RxJava2 , Tôi đã thoát khỏi Ranh giới giao diện (như một cách để đảm bảo Quy tắc phụ thuộc thực thi) ủng hộ tiêu chuẩn Có thể quan sát Người đăng ký .

Ngoài ra, sử dụng RxJava2 công cụ Tôi đã tích hợp chuyển đổi dữ liệu để làm việc linh hoạt hơn với nó. Nó liên quan đến cả việc làm việc với phản hồi của máy chủ và với cơ sở dữ liệu.

Ngoài mô hình chính, tôi sẽ tạo mô hình phản hồi máy chủ và mô hình bảng riêng cho Phòng . Chuyển đổi dữ liệu giữa hai mô hình này, bạn có thể thực hiện bất kỳ thay đổi nào trong quá trình chuyển đổi, chuyển đổi phản hồi của máy chủ và lưu dữ liệu cần thiết vào cơ sở trước khi hiển thị trên giao diện người dùng, v.v.

Các phân đoạn chịu trách nhiệm về Giao diện người dùng và ViewModel Fragment chịu trách nhiệm thực thi logic nghiệp vụ. Nếu logic nghiệp vụ liên quan đến toàn bộ hoạt động, thì Hoạt động ViewModel.

ViewModels lấy dữ liệu từ một nhà cung cấp bằng cách khởi tạo nó qua val… by lazy {}, nếu bạn cần một đối tượng bất biến hoặc lateinit var, nếu ngược lại. Sau khi thực thi logic nghiệp vụ, nếu bạn cần chuyển dữ liệu để thay đổi Giao diện người dùng, bạn tạo MutableLiveData mới trong ViewModel mà bạn sẽ sử dụng trong ObserLiveData () phương pháp của Fragment của chúng tôi.

Nghe có vẻ khá dễ dàng. Việc thực hiện cũng đơn giản.
Một thành phần thiết yếu trong kiến ​​trúc của chúng tôi là bộ chuyển đổi dữ liệu dựa trên một chuyển đổi đơn giản từ kiểu dữ liệu này sang kiểu dữ liệu khác. Để chuyển đổi RxJava luồng dữ liệu, SingleTransformer hoặc FlowableTransformer được sử dụng tùy thuộc vào loại. Trong trường hợp ứng dụng của chúng tôi, giao diện và lớp trừu tượng của trình chuyển đổi trông giống như sau:

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
}

Trong ví dụ này, tôi sử dụng các chuyển đổi cơ bản như mô hình-mô hình, danh sách các mô hình - danh sách các mô hình và các kết hợp giống nhau nhưng chỉ sử dụng SingleTransformer để xử lý các phản hồi và yêu cầu của máy chủ trong cơ sở dữ liệu.

Hãy bắt đầu với mạng - với RestClient. retrofitBuilder phương thức sẽ như sau:

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"

Sử dụng API của bên thứ ba, luôn có cơ hội nhận được phản hồi rỗng tuyệt đối từ máy chủ và có thể có nhiều lý do cho điều đó. Đó là lý do tại sao NullOrEmptyConverterFactory bổ sung sẽ giúp xử lý tình huống. Đây là cách nó trông:

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) }
            }
        }
    }
}

Để tạo các mô hình, cần phải xây dựng trên một API. Ví dụ:tôi sẽ sử dụng APU miễn phí cho mục đích phi thương mại từ newsapi.org. Nó có một danh sách khá phong phú các chức năng được yêu cầu, nhưng tôi sẽ sử dụng một phần nhỏ cho ví dụ này. Sau khi đăng ký nhanh, bạn có quyền truy cập vào API và khóa api của mình được yêu cầu cho mỗi yêu cầu.

Là điểm cuối, tôi sẽ sử dụng https://newsapi.org/v2/everything . Từ truy vấn được đề xuất Tôi chọn như sau: q - truy vấn tìm kiếm, từ - sắp xếp từ ngày, đến - sắp xếp theo ngày tháng, sortBy - sắp xếp theo tiêu chí đã chọn và phải có apiKey.

Sau RestClient tạo, tôi tạo giao diện API với Truy vấn đã chọn cho ứng dụng của chúng tôi:

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"

Chúng tôi sẽ nhận được phản hồi này trong 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)

Những dữ liệu này từ toàn bộ phản hồi sẽ đủ để hiển thị một danh sách với hình ảnh, tiêu đề và mô tả tin tức.

Để triển khai cách tiếp cận kiến ​​trúc của chúng tôi, hãy tạo các mô hình chung:

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

Vì mô hình Article sẽ được sử dụng cho kết nối với cơ sở dữ liệu và dữ liệu hiển thị trong bộ điều hợp, chúng tôi cần thêm 2 lề mà tôi sẽ sử dụng để thay đổi các phần tử giao diện người dùng trong danh sách.

Khi mọi thứ đã sẵn sàng cho yêu cầu, tôi tạo bộ chuyển đổi cho các mô hình mạng mà chúng tôi sẽ sử dụng trong truy vấn nhận tin tức qua NetworkModule.

Các trình chuyển đổi được tạo theo thứ tự ngược lại từ lồng và chúng chuyển đổi theo thứ tự trực tiếp cho phù hợp. Vì vậy, cái đầu tiên tôi tạo trên Bài báo, cái thứ hai trên Tin tức:

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) })
    }
}

Như bạn có thể thấy ở trên, trong quá trình chuyển đổi đối tượng Tin tức, việc chuyển đổi danh sách đối tượng Bài viết cũng được thực hiện.

Sau khi tạo bộ chuyển đổi cho các mô hình mạng, chúng ta hãy tiến hành tạo mô-đun (mạng kho lưu trữ). Vì thường có nhiều hơn 1 hoặc 2 API giao diện, bạn cần tạo BaseModule, API đã nhập, Mô-đun mạng và Mô-đun chuyển đổi.

Đây là cách nó trông:

abstract class BaseNetworkModule<A, NM, M>(val api: A, val dataConverter: BaseDataConverter<NM, M>)

Theo đó, nó sẽ như sau trên 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())
}

Đối với API này, khóa API là một tham số quan trọng để yêu cầu bởi bất kỳ điểm cuối được đề xuất nào. Đó là lý do tại sao bạn cần đảm bảo rằng các thông số tùy chọn sẽ không được chỉ định trước và bạn cần vô hiệu hóa chúng theo mặc định.

Như bạn có thể thấy ở trên, tôi đã áp dụng chuyển đổi dữ liệu trong quá trình xử lý phản hồi.

Hãy làm việc với cơ sở dữ liệu. Tôi tạo cơ sở dữ liệu ứng dụng, gọi nó là AppDatabase và kế thừa từ RoomDatabase () .

Để khởi tạo cơ sở dữ liệu, cần tạo DatabaseCreator , sẽ được khởi chạy trong Ứng dụng lớp học.

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() })
    }
}

Bây giờ trong onCreate () phương pháp của Ứng dụng lớp tôi khởi tạo Stetho và cơ sở dữ liệu:

override fun onCreate() {
        super.onCreate()
        instance = this
        Stetho.initializeWithDefaults(this)
        DatabaseCreator.createDatabase(this)
    }

Khi cơ sở dữ liệu được tạo, tôi tạo một Dao cơ bản với một phương thức insert () bên trong:

@Dao
interface BaseDao<in I> {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(obj: I)
}

Dựa trên ý tưởng về ứng dụng của chúng tôi, tôi sẽ lưu tin tức tôi thích, lấy danh sách các bài báo đã lưu, xóa tin tức đã lưu theo ID của nó hoặc xóa tất cả tin tức khỏi bảng. NewsDao của chúng tôi sẽ như sau:

@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()
}

Và bảng tin tức sẽ như sau:

@Entity(tableName = NEWS_TABLE)
data class NewsDatabase(@PrimaryKey var id: Long?,
                        var title: String?,
                        var description: String?,
                        var urlToImage: String?)

Khi bảng được tạo, hãy liên kết nó với cơ sở dữ liệu:

@Database(entities = [NewsDatabase::class], version = DB_VERSION)
abstract class AppDatabase : RoomDatabase() {

    abstract fun newsDao(): NewsDao
}

Giờ đây, chúng tôi có thể làm việc với cơ sở dữ liệu, lưu và trích xuất dữ liệu từ nó.

Đối với mô-đun (mạng kho lưu trữ), tôi sẽ tạo một trình chuyển đổi mô hình - mô hình bảng cơ sở dữ liệu:

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 có sẵn để làm việc với các bảng khác nhau. Hãy viết nó. Nó sẽ trông giống như sau trong phiên bản đơn giản nhất của nó, đủ cho ứng dụng:

abstract class BaseRepository<M, DBModel> {

    protected abstract val dataConverter: BaseDataConverter<M, DBModel>
    protected abstract val dao: BaseDao<DBModel>
}

Sau khi tạo BaseRepository, tôi sẽ tạo 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())
}

Khi các mô-đun và kho lưu trữ vĩnh viễn được tạo, dữ liệu sẽ chuyển từ nhà cung cấp ứng dụng sẽ yêu cầu dữ liệu từ mạng hoặc cơ sở dữ liệu tùy thuộc vào yêu cầu. Một nhà cung cấp nên kết hợp cả hai kho. Xem xét khả năng của các mô hình và kho lưu trữ khác nhau, tôi sẽ tạo 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
}


Sau đó, NewsProvider sẽ giống như sau:

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()
}

Bây giờ chúng ta sẽ nhận được danh sách các tin tức một cách dễ dàng. Trong NewsViewModel chúng tôi sẽ khai báo tất cả các phương pháp của nhà cung cấp của chúng tôi để sử dụng thêm:

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)
    }

Sau khi khai báo tất cả các phương thức thực thi logic nghiệp vụ trong ViewModel, chúng tôi sẽ gọi chúng trở lại từ Fragment ở vị trí trong ObserLiveData () kết quả của từng Dữ liệu trực tiếp được khai báo sẽ được xử lý.

Để triển khai dễ dàng, trong SEARCH_FOR thông số tôi đã chọn ngẫu nhiên Apple, và sắp xếp thêm sẽ được thực hiện theo mức độ phổ biến nhãn. Nếu cần, bạn có thể thêm chức năng tối thiểu để thay đổi các thông số này.

Vì newsapi.org không cung cấp cho bạn ID tin tức, nên tôi chấp nhận chỉ mục phần tử làm ID. Sắp xếp theo thẻ phổ biến cũng được triển khai thông qua API. Nhưng để tránh ghi lại dữ liệu với các ID giống nhau trong cơ sở trong quá trình sắp xếp theo mức độ phổ biến, tôi sẽ xác minh tính khả dụng của dữ liệu trong cơ sở trước khi tải danh sách tin tức. Nếu cơ sở trống - danh sách mới đang tải, nếu không - thông báo sẽ hiển thị.

Hãy gọi trong onViewCreate () phương pháp của NewsFragment phương pháp sau:

private fun loadLikedNews() {
        viewModel.loadLikedNews()
    }

Vì cơ sở của chúng tôi trống, phương thức loadNews () sẽ được đưa ra. Trong ObserLiveData phương pháp Tôi sẽ sử dụng LiveData đang tải của chúng tôi - viewModel.loadNewsSuccessLiveData.observe (..) {news →}, nơi chúng tôi sẽ nhận được danh sách các bài báo nếu yêu cầu thành công và sau đó chuyển nó đến bộ điều hợp:

isEmptyDataPlaceholderLiveData.value = news.articles?.isEmpty()
                with(newsAdapter) {
                    news.articles?.toMutableList()?.let {
                        clear()
                        addAll(it)
                    }
                    notifyDataSetChanged()
                }
                loadNewsSuccessLiveData.value = null

Sau khi khởi chạy ứng dụng, bạn sẽ thấy kết quả sau:

Cách đơn giản hóa kiến ​​trúc ứng dụng Android của bạn:Hướng dẫn chi tiết với các mẫu mã

Trong menu thanh công cụ ở phía bên phải, bạn có thể thấy 2 tùy chọn - sắp xếp và yêu thích. Hãy sắp xếp danh sách theo mức độ phổ biến và nhận được kết quả sau:

Cách đơn giản hóa kiến ​​trúc ứng dụng Android của bạn:Hướng dẫn chi tiết với các mẫu mã

Nếu bạn đi tới Mục ưa thích, bạn sẽ chỉ thấy một Trình giữ chỗ, vì không có dữ liệu nào trong cơ sở. Màn hình Yêu thích sẽ giống như sau:

Cách đơn giản hóa kiến ​​trúc ứng dụng Android của bạn:Hướng dẫn chi tiết với các mẫu mã

Phân đoạn giao diện người dùng của Mục ưa thích có màn hình hiển thị danh sách tin tức được yêu thích và chỉ có một tùy chọn trong thanh công cụ để dọn dẹp cơ sở dữ liệu. Khi bạn lưu dữ liệu bằng cách nhấp vào “Thích”, màn hình sẽ giống như sau:

Cách đơn giản hóa kiến ​​trúc ứng dụng Android của bạn:Hướng dẫn chi tiết với các mẫu mã

Như tôi đã viết ở trên, trong mô hình tiêu chuẩn, 2 biên bổ sung đã được thêm vào mô hình chung và những biên này được sử dụng để hiển thị dữ liệu trong bộ điều hợp. Bây giờ bạn có thể thấy rằng các phần tử của tin tức đã lưu không có tùy chọn để thêm vào Mục yêu thích.

var isAddedToFavorite: Boolean?
    var fragmentName: FragmentsNames?

Nếu bạn nhấp lại vào “Thích”, phần tử đã lưu sẽ bị xóa khỏi cơ sở.

Kết thúc

Như vậy, tôi đã chỉ cho bạn một cách tiếp cận đơn giản và rõ ràng để phát triển ứng dụng Android. Chúng tôi theo kịp các nguyên tắc chính của Kiến trúc sạch nhưng đã đơn giản hóa nó nhiều nhất có thể.

Sự khác biệt giữa kiến ​​trúc mà tôi đã cung cấp cho bạn và Kiến trúc sạch từ ông Martin là gì? Ngay từ đầu, tôi đã lưu ý rằng kiến ​​trúc của tôi tương tự như CA vì nó được sử dụng làm cơ sở. Đây là lược đồ CA bên dưới:

Cách đơn giản hóa kiến ​​trúc ứng dụng Android của bạn:Hướng dẫn chi tiết với các mẫu mã

Sự kiện được chuyển đến Người trình bày, sau đó đến Trường hợp sử dụng. Trường hợp sử dụng yêu cầu Kho lưu trữ. Hệ thống lưu trữ nhận dữ liệu, tạo Thực thể, và chuyển nó đến UseCase. Do đó, Trường hợp sử dụng nhận tất cả các Thực thể cần thiết. Sau khi triển khai logic nghiệp vụ, bạn sẽ nhận được kết quả quay lại Người trình bày, và đến lượt nó, chuyển kết quả sang Giao diện người dùng.

Trong lược đồ bên dưới, Bộ điều khiển gọi các phương thức từ InputPort triển khai UseCase Cổng ra giao diện nhận được phản hồi này và Người trình bày triển khai nó. Thay vì UseCase trực tiếp tùy thuộc vào Người trình bày, nó phụ thuộc vào giao diện trong các lớp của nó và nó không mâu thuẫn với Quy tắc phụ thuộc và Người trình bày nên triển khai giao diện này.

Cách đơn giản hóa kiến ​​trúc ứng dụng Android của bạn:Hướng dẫn chi tiết với các mẫu mã

Do đó, các quy trình được triển khai ở lớp bên ngoài không ảnh hưởng đến các quy trình ở lớp bên trong. Thực thể trong Kiến trúc sạch là gì? Trên thực tế, nó là mọi thứ không phụ thuộc vào một ứng dụng cụ thể và nó sẽ là một khái niệm chung cho nhiều ứng dụng. Nhưng trong quá trình phát triển di động Thực thể là các đối tượng nghiệp vụ của ứng dụng, chứa các quy tắc chung và cấp cao (logic nghiệp vụ của ứng dụng).

Còn về Cổng? Như tôi thấy, Cổng vào là một kho lưu trữ để làm việc với cơ sở dữ liệu và một mô-đun để làm việc với mạng. Chúng tôi đã loại bỏ bộ điều khiển vì ban đầu Kiến trúc sạch được tạo ra để cấu trúc các ứng dụng kinh doanh có độ phức tạp cao và bộ chuyển đổi dữ liệu thực hiện các chức năng của nó trong ứng dụng của tôi. ViewModels chuyển dữ liệu sang Fragment để xử lý giao diện người dùng thay thế cho Người trình bày.

Trong cách tiếp cận của mình, tôi cũng tuân thủ Quy tắc phụ thuộc một cách nghiêm ngặt và logic của các kho, mô-đun, mô hình và trình cung cấp được đóng gói và có thể truy cập vào chúng thông qua các giao diện. Do đó, những thay đổi trong các lớp bên ngoài không ảnh hưởng đến các lớp bên trong. Và quy trình triển khai sử dụng RxJava2 , KotlinRx Kotlin LiveData làm cho các nhiệm vụ của nhà phát triển dễ dàng hơn, rõ ràng hơn và mã trở nên dễ đọc và dễ dàng mở rộng.