Android
 Computer >> コンピューター >  >> システム >> Android

Android アプリのアーキテクチャを簡素化する方法:コード サンプルを含む詳細ガイド

個々のプログラマーは、さまざまなタスクの実行方法に関するアイデアや見解など、自分のビジョンに従ってモバイル アプリを開発します。オブジェクト指向または関数型プログラミングの主な原則を無視する場合があり、開発者の間で混乱を招く可能性があります。

これは悪いことです - 彼らは彼らのコードを扱うことができなくなります.そして、プロジェクトを維持または変更する必要がある次の開発者は気が狂うかもしれません。メンテナンスは複雑なプロセスになるため、このようなプロジェクトを最初から再構築することをお勧めします。

Google がサポートする最初のアーキテクチャをリリースするまで、ほぼすべてのソフトウェア開発会社が独自のアーキテクチャを使用していました。これにより、コードがより明確になり、プロジェクト間の切り替えが可能になりました。しかし、開発者が会社を変更した場合、新しいプロジェクトと共にその新しいアーキテクチャを習得するには、ある程度の時間がかかります。

現時点では、Google のおかげで、Android 開発者向けに 16 の異なるアーキテクチャがあります。

  • 6 つの安定したサンプル (Java);
  • 2 つの安定したサンプル (Kotlin):
  • 4 つの外部サンプル;
  • 3 つの非推奨サンプル;
  • 1 つのサンプルが進行中です。

使用するアーキテクチャは、さまざまな機能を実装するためのさまざまなツールキットの特定の目的、アプローチ、およびアプリケーションによって異なります。そしてそれはプログラミング言語に依存します。

ただし、これらすべてのアーキテクチャには、ネットワーク、データベース、依存関係、およびコールバックの処理に関するロジックをほぼ均等に分割する 1 つの共通のアーキテクチャ基盤があります。

プロセス中に使用されるツール

これらすべてのアーキテクチャを検討した後、単純化されたアプローチを構築し、レイヤー数の少ないアーキテクチャを思い付きました。ニュース リストを読み込み、記事をお気に入りに保存し、必要に応じて私のアプローチを使用して削除できるシンプルな Android アプリを実装する方法を紹介します。

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、 に抽出する必要があります build.gradle の Kotlin のすぐに使えるバージョニングの例でわかるように アプリレベルで。そして、そこに 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 でさらに実装される依存関係 Massif を作成します アプリレベルで。次のようになります:

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

バージョンと大規模な名前はランダムに実装されます。その後、依存関係を 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)
    }
}

2 番目のステップは、各アクティビティまたはフラグメントに使用する 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 を使用します .

アプリ内の任意の場所でエラーを処理するには、 Throwable.message を転送する Consumer を作成する必要があります 値を 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
        }
    }
}


このアプリでは、2 つの画面 (ニュース一覧画面とお気に入り一覧画面) 用にいくつかのアクティビティを作成しても意味がありません。しかし、このサンプルは最適で拡張しやすいアーキテクチャの実装を示しているため、基本的なアプリを作成します。

私たちのアプリは、コンテナ アクティビティで膨らませる 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>

どこで include_placeholderinclude_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)
    }
}

今後のすべてのアクティビティのプロセスで発生する可能性のあるエラーを表示する方法を実装しましょう。簡単にするために、通常のトーストの形で行います。

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 の変更についていくことから始めます 基本ビュー モデルにある値。 startObserveLiveData() メソッドは次のように変更されます:

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

onErrorConsumer を使用中 onError としての基本的な ViewModel の 実装されたメソッド エラーに関するメッセージが表示されます。

アクティビティ内のフラグメントをバック スタックに追加する機能に置き換えることができるメソッドを作成します。

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 を設定する関数を作成し、これらのビューも初期化しました。

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 を設定します:

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

基本的な ViewModel と Basic Activity の作成について詳しく説明しました。基本フラグメントは、同じ原則に従って作成されます。

それぞれ別の画面を作成するときに、さらなる拡張と可能な変更を検討している場合は、その ViewModel を使用して別の Fragment を作成する必要があります。

注:フラグメントを 1 つのクラスターに組み合わせることができ、ビジネス ロジックがそれほど複雑ではない場合、複数のフラグメントが 1 つの ViewModel を使用することがあります。

フラグメント間の切り替えは、Activity に実装されているインターフェイスのために発生します。これを行うには、各フラグメントに コンパニオン オブジェクト{ } が必要です。 Bundle に引数を転送する機能を備えた Fragment オブジェクト構築のメソッドを使用 :

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

アーキテクチャ ソリューション

基本的なコンポーネントが作成されたら、アーキテクチャに焦点を当てます。概略的には、有名なロバート C. マーティンやボブおじさんが作ったすっきりとした建築のように見えます。でも RxJava2 を使っているので 、境界を取り除きました インターフェイス ( 依存性ルール を保証する方法として) 実行) 標準の Observable を優先 と購読者 .

これとは別に、 RxJava2 を使用して ツール より柔軟な作業のためにデータ変換を統合しました。これは、サーバー応答とデータベースの両方に関係しています。

プライマリ モデルに加えて、サーバー レスポンス モデルと別のテーブル モデルを Room 用に作成します。 .これら 2 つのモデル間でデータを変換すると、変換プロセス中に変更を加えたり、サーバーの応答を変換したり、必要なデータを UI に表示する前にベースに保存したりできます。

フラグメントは UI を担当します 、および ViewModel Fragments は、ビジネス ロジックの実行を担当します。ビジネス ロジックがアクティビティ全体に関係する場合は、ViewModel アクティビティ。

ViewModel は、val … by lazy{}、 を介して初期化することにより、プロバイダーからデータを取得します。 不変オブジェクトまたは lateinit var が必要な場合 逆の場合。ビジネス ロジックの実行後、データを転送して UI を変更する必要がある場合は、 新しい MutableLiveData を作成します observeLiveData() で使用する ViewModel で 私たちのフラグメントのメソッド。

とても簡単に聞こえます。実装も簡単です。
私たちのアーキテクチャの重要なコンポーネントは、あるデータ型から別のデータ型への単純な変換に基づくデータ コンバーターです。 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 のみを使用します。 データベース内のサーバー応答と要求の処理用。

ネットワークから始めましょう - with 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 を使用すると、サーバーから完全な null 応答を受け取る可能性が常にあり、それにはさまざまな理由が考えられます。そのため、追加の 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 上に構築する必要があります。例として、newsapi.org の非営利目的の無料 APU を使用します。 要求された機能のかなり広範なリストがありますが、この例ではごく一部を使用します。簡単な登録後、API と API キー にアクセスできます これは、リクエストごとに必要です。

エンドポイントとして、https://newsapi.org/v2/everything を使用します <強い>。 提案された クエリ から 次を選択します:q - 検索クエリ、から - 日付から への並べ替え - 現在までの並べ替え、sortBy - 選択した基準による並べ替え、および必須の apiKey.

RestClient の後 アプリ用に選択したクエリを使用して API インターフェイスを作成します。

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 モデルは、データベースとの接続とアダプターでのデータ表示に使用されるため、リスト内の UI 要素を変更するために使用する 2 つのマージンを追加する必要があります。

リクエストの準備がすべて整ったら、NetworkModule を介して受信するニュースのクエリで使用するネットワーク モデルのコンバーターを作成します。

コンバーターはネストから逆の順序で作成され、それに応じて直接の順序で変換されます。最初は Article で作成し、2 つ目は 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) })
    }
}

上記のように、ニュース オブジェクトの変換中に、記事オブジェクト リストの変換も実行されます。

ネットワーク モデルのコンバーターが作成されたら、モジュール (リポジトリ ネットワーク) の作成に進みましょう。通常、インターフェース 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()アプリのメソッド Stetho を初期化するクラス およびデータベース:

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

データベースが作成されたら、内部に単一の insert() メソッドを含む基本的な Dao を作成します。

@Dao
interface BaseDao<in I> {

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

私たちのアプリのアイデアに基づいて、好きなニュースを保存したり、保存した記事のリストを取得したり、保存したニュースを ID で削除したり、テーブルからすべてのニュースを削除したりします。私たちの 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 でビジネス ロジックを実行するすべてのメソッドを宣言したら、observeLiveData() の Fragment から呼び出します。 宣言された各 LiveData の結果 処理されます。

SEARCH_FOR で簡単に実装するには パラメータ Apple、 を無作為に選択しました 人気度でさらに並べ替えが行われます 鬼ごっこ。必要に応じて、これらのパラメーターを変更するための最小限の機能を追加できます。

newsapi.org はニュース ID を提供しないので、要素のインデックスを ID として受け入れます。人気タグによるソートも API 経由で実装されています。ただし、人気順のソート中にベース内の同じ ID でデータが書き換えられるのを避けるために、ニュース リストをロードする前にベース内のデータの可用性を確認します。ベースが空の場合 - 新しいリストがロードされている場合、そうでない場合 - 通知が表示されます。

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

アプリを起動すると、次の結果が表示されます:

Android アプリのアーキテクチャを簡素化する方法:コード サンプルを含む詳細ガイド

右側のツールバー メニューには、並べ替えとお気に入りの 2 つのオプションがあります。リストを人気順に並べ替えて、次の結果を取得しましょう:

Android アプリのアーキテクチャを簡素化する方法:コード サンプルを含む詳細ガイド

お気に入りに移動すると、ベースにデータがないため、プレースホルダーのみが表示されます。 [お気に入り] 画面は次のようになります:

Android アプリのアーキテクチャを簡素化する方法:コード サンプルを含む詳細ガイド

お気に入りの UI フラグメントには、お気に入りのニュースのリストを表示するための画面と、データベース クリーニング用のツールバーのオプションが 1 つだけあります。 「いいね」をクリックしてデータを保存すると、次のような画面が表示されます。

Android アプリのアーキテクチャを簡素化する方法:コード サンプルを含む詳細ガイド

上で書いたように、標準モデルでは一般モデルに 2 つの追加マージンが追加され、これらのマージンはアダプターでのデータ表示に使用されます。保存されたニュース リストの要素には、お気に入りに追加するオプションがないことがわかります。

var isAddedToFavorite: Boolean?
    var fragmentName: FragmentsNames?

もう一度「いいね」をクリックすると、保存された要素がベースから削除されます。

まとめ

このように、Android アプリ開発へのシンプルで明確なアプローチを示しました。私たちはクリーン アーキテクチャの主な原則に遅れずについていきましたが、可能な限り単純化しました。

私が提供したアーキテクチャとマーティン氏のクリーン アーキテクチャの違いは何ですか?最初に、私のアーキテクチャは CA がベースとして使用されているため、CA に似ていることに気付きました。以下は CA スキームです:

Android アプリのアーキテクチャを簡素化する方法:コード サンプルを含む詳細ガイド

イベントは Presenter に移動し、次に Use Case に移動します。使用例 リポジトリをリクエストします。 リポジトリがデータを受け取り、エンティティを作成 ユースケースに転送します。 したがって、 ユースケース 必要なすべてのエンティティを受け取ります。ビジネス ロジックの実装後、Presenter、 に返される結果が得られます。 次に、結果を UI に転送します。

以下のスキームでは、 Controller InputPort からメソッドを呼び出します ユースケースを実装する 、および OutputPort インターフェースがこの応答を受け取り、Presenter 実装します。 ユースケースの代わりに 発表者に応じて直接 レイヤー内のインターフェースに依存し、依存関係ルールと矛盾しません プレゼンターはこのインターフェイスを実装する必要があります。

Android アプリのアーキテクチャを簡素化する方法:コード サンプルを含む詳細ガイド

したがって、外部レイヤーで実装されたプロセスは、内部レイヤーのプロセスに影響しません。クリーン アーキテクチャのエンティティとは実際、特定のアプリに依存しないすべてのものであり、多くのアプリの一般的な概念になります。しかし、モバイル開発プロセスでは、エンティティはアプリのビジネス オブジェクトであり、一般的で高レベルなルール (アプリ ビジネス ロジック) が含まれています。

ゲートウェイはどうですか? 私の考えでは、ゲートウェイ データベースを操作するためのリポジトリと、ネットワークを操作するためのモジュールです。最初にクリーン アーキテクチャが非常に複雑なビジネス アプリを構築するために作成され、データ コンバーターが私のアプリでその機能を実行するため、コントローラーを取り除きました。 ViewModel は、プレゼンターを置き換える UI 処理のためにデータを Fragments に転送します。

私のアプローチでは、Dependency Rule も厳密に守っており、リポジトリ、モジュール、モデル、およびプロバイダーのロジックはカプセル化されており、インターフェイスを介してそれらにアクセスできます。したがって、外部レイヤーの変更は内部レイヤーには影響しません。 RxJava2 を使用した実装プロセス 、KotlinRx 、および Kotlin LiveData 開発者のタスクがより簡単かつ明確になり、コードが読みやすくなり、簡単に拡張できるようになります。


  1. Android でデフォルトのアプリを変更する方法

    Android は、豊富なアプリ ライブラリで人気があります。 Play ストアには、同じタスクを実行できる数百のアプリが用意されています。すべてのアプリには、Android ユーザーごとに異なる独自の機能セットがあります。すべての Android デバイスには、インターネットの閲覧、ビデオの視聴、音楽の鑑賞、ドキュメントの操作などのさまざまなアクティビティを実行するのに役立つ独自の既定のアプリ セットが付属していますが、それらはめったに使用されません。人々は、快適で使い慣れた別のアプリを使用することを好みます。したがって、同じタスクを実行する複数のアプリが同じデバイス上に存在します。

  2. Android で友人と位置情報を共有する方法

    GPS システムは、ここ数十年で大きな進歩を遂げました。このテクノロジーは、Android スマートフォンから簡単にアクセスできるようになりました。正確な位置を確認できるアプリはいくつかあります。最も人気があり広く使用されているのは、もちろん Google マップです。これらのアプリを使用すると、現在地を見つけてナビゲートするだけでなく、この場所を友達と共有することもできます。誰もがこれに慣れていないか、この機能の使用方法を知らないため、Android で友人と位置情報を共有する方法について説明します。 あなたの位置を直接共有することは、あなたの正確な居場所を友達と通信するための非常に便利