Kotlinでの最新のAndroid開発。 パート2

こんにちは、Habr! Mladen Rakonjacによる記事「 Kotlinによる最新のAndroid開発(パート2) 」の翻訳を紹介します。

ご注意 この記事は、 Mladen Rakonjacの一連の記事の翻訳です(記事日付: 2017年9月23日)。 Github SemperPeritus最初の部分を読み始めたので、残りの部分が何らかの理由で翻訳されていないこと発見しました。 したがって、私はあなたの注意に第二部をもたらします。 記事は膨大であることが判明しました。

画像

「Android Studio 3.0でAndroidの開発のすべてをカバーするプロジェクトを見つけるのは非常に難しいので、私はそれを書くことにしました。」

この記事では、以下を分析します。

  1. Android Studio 3ベータ1 パート1
  2. Kotlinプログラミング言語パート1
  3. ビルドオプションパート1
  4. ConstraintLayout パート1
  5. データバインディングライブラリパート1
  6. MVVMアーキテクチャ+リポジトリ+ Androidマネージャーラッパーパターン
  7. RxJava2とそれがパート3アーキテクチャでどのように役立つか
  8. Dagger 2.11、依存性注入とは何ですか、このパート4を使用する理由
  9. レトロフィット(Rx Java2を使用)
  10. ルーム(Rx Java2を使用)

MVVMアーキテクチャ+リポジトリ+ Androidマネージャーラッパーパターン


Androidの世界のアーキテクチャについて一言


かなり長い間、Android開発者はプロジェクトでアーキテクチャを使用していません。 過去3年間で、Android開発者のコ​​ミュニティで彼女の周りに多くの誇大宣伝が出てきました。 God Activityの時代は過ぎ去り、GoogleはAndroid Architecture Blueprintsリポジトリを公開しました。これには、さまざまなアーキテクチャアプローチに関する多くの例と指示が含まれています。 最後に、Google IO '17で、彼らはAndroid Architecture Componentsを導入しました。これは、よりクリーンなコードの作成とアプリケーションの改善を支援するために設計されたライブラリのコレクションです。 コンポーネントは、それらのすべてを使用することも、1つだけを使用することもできると言います。 しかし、それらはすべて非常に有用であることがわかりました。 さらに本文と以下の部分でそれらを使用します。 まず、コードの問題に取り組み、次にこれらのコンポーネントとライブラリを使用してリファクタリングし、解決すべき問題を確認します。

GUIコードを共有する2つの主要なアーキテクチャパターンがあります。


どちらが良いかを言うのは難しいです。 両方を試して決定する必要があります。 ライフサイクル対応のコンポーネントを使用するMVVMを好み、それについて書きます。 MVPを使用したことがない場合は、Mediumでこれに関する多くの優れた記事があります。

MVVMパターンとは何ですか?


MVVMは、Model-View-ViewModelとして拡張されるアーキテクチャパターンです。 この名前は開発者を混乱させると思います。 私が彼の名前を思いついたのであれば、 ViewModelViewModelを接続しているため、 ViewModelを View-ViewModel-Modelと呼びます。

ビューは、 アクティビティフラグメント、またはその他のカスタムビュー( Androidカスタムビュー )の抽象化です。 このビューとAndroidビューを混同しないことが重要です。 ビューは馬鹿げているべきであり、それにロジックを書くべきではありません。 ビューにデータを含めることはできません。 ViewModelインスタンスへの参照と、 Viewが必要とするすべてのデータをそこから格納する必要があります。 さらに、 ビューはこのデータを監視する必要があり、 ViewModelのデータが変更されるとレイアウトが変更される必要があります。 要約すると、 Viewは次の役割を果たします。さまざまなデータと状態のレイアウトビュー。

ViewModelは、データとロジックを含むクラスの抽象名です。このデータを受信するタイミングと表示するタイミングです。 ViewModelは現在の状態を保存しますViewModelは、1つ以上のModelへのリンクも保存し、それらからすべてのデータを受け取ります。 たとえば、データがどこから来たのか、データベースから来たのか、サーバーから来たのかを知るべきではありません。 さらに、 ViewModelViewについて何も知る必要がありません。 さらに、 ViewModelはAndroidフレームワークについて何も知らないはずです。

Modelは、 ViewModelのデータを準備するレイヤーの抽象名です。 これは、サーバーからデータを受信して​​キャッシュするか、ローカルデータベースに保存するクラスです。 これらは、User、Car、Square、単純にデータを保存する他のモデルクラスと同じクラスではないことに注意してください。 原則として、これはリポジトリテンプレートの実装であり、後で検討します。 モデルViewModelについて何も知らないはずです。

MVVMは 、正しく実装されていれば、コードを破壊してテストしやすくする優れた方法です。 これにより、 SOLID原則に従うことができるため、コードの保守が容易になります。

コード例


次に、これがどのように機能するかを示す簡単な例を作成します。

始めるために、行を返す簡単なモデルを作成しましょう:

RepoModel.kt
class RepoModel { fun refreshData() : String { return "Some new data" } } 


通常、データ取得は非同期呼び出しであるため、待機する必要があります。 これをシミュレートするために、クラスを次のように変更しました。

RepoModel.kt
 class RepoModel { fun refreshData(onDataReadyCallback: OnDataReadyCallback) { Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000) } } interface OnDataReadyCallback { fun onDataReady(data : String) } 


onDataReadyメソッドでOnDataReadyCallbackインターフェイスを作成しました。 そして今、 refreshDataメソッドはrefreshDataを実装(実装)していOnDataReadyCallback 。 待機をシミュレートするには、 Handlerを使用します。 2秒に1 onDataReadyOnDataReadyCallbackインターフェイスを実装するクラスでonDataReadyメソッドが呼び出されます。

ViewModelを作成しましょう:

MainViewModel.kt
 class MainViewModel { var repoModel: RepoModel = RepoModel() var text: String = "" var isLoading: Boolean = false } 


ご覧のRepoModeltextされるRepoModeltextインスタンス、および現在の状態を格納する変数isLoadingがあります。 データを取得するrefreshメソッドを作成しましょう。

MainViewModel.kt
 class MainViewModel { ... val onDataReadyCallback = object : OnDataReadyCallback { override fun onDataReady(data: String) { isLoading.set(false) text.set(data) } } fun refresh(){ isLoading.set(true) repoModel.refreshData(onDataReadyCallback) } } 


refreshメソッドは、引数にOnDataReadyCallback実装をとるRepoModelrefreshDataを呼び出します。 わかりましたが、 objectとは何ですか? インターフェイスを実装するか、サブクラス化せずに拡張クラスを継承する場合は常に、 オブジェクト宣言を使用します 。 そして、これを匿名クラスとして使用したい場合は? この場合、 オブジェクト式を使用しています

MainViewModel.kt
 class MainViewModel { var repoModel: RepoModel = RepoModel() var text: String = "" var isLoading: Boolean = false fun refresh() { repoModel.refreshData( object : OnDataReadyCallback { override fun onDataReady(data: String) { text = data }) } } 


refreshを呼び出すときは、ビューを読み込み状態に変更し、データが到着しisLoadingfalseに設定するisLoadingがありfalse

また、 text
 ObservableField<String> 
、およびisLoading on
 ObservableField<Boolean> 
ObservableFieldは、Observableオブジェクトを作成する代わりに使用できるデータバインディングライブラリのクラスであり、監視するオブジェクトをラップします。

MainViewModel.kt
 class MainViewModel { var repoModel: RepoModel = RepoModel() val text = ObservableField<String>() val isLoading = ObservableField<Boolean>() fun refresh(){ isLoading.set(true) repoModel.refreshData(object : OnDataReadyCallback { override fun onDataReady(data: String) { isLoading.set(false) text.set(data) } }) } } 


varの代わりにvalを使用していることに注意してください。フィールドの値のみを変更し、フィールド自体は変更しないためです。 また、初期化する場合は、次を使用します。

initobserv.kt
 val text = ObservableField("old data") val isLoading = ObservableField(false) 



テキストisLoadingを監視できるようにレイアウトを変更しましょう。 開始するには、 RepositoryではなくMainViewModelをバインドします

activity_main.xml
 <data> <variable name="viewModel" type="me.mladenrakonjac.modernandroidapp.MainViewModel" /> </data> 


次に:


main_activity.xml
 ... <TextView android:id="@+id/repository_name" android:text="@{viewModel.text}" ... /> ... <ProgressBar android:id="@+id/loading" android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" ... /> <Button android:id="@+id/refresh_button" android:onClick="@{() -> viewModel.refresh()}" android:clickable="@{viewModel.isLoading ? false : true}" /> ... 


ここで実行すると、 View.VISIBLE and View.GONE cannot be used if View is not importedView.VISIBLE and View.GONE cannot be used if View is not imported 。 さて、インポートしましょう:

main_activity.xml
 <data> <import type="android.view.View"/> <variable name="viewModel" type="me.fleka.modernandroidapp.MainViewModel" /> </data> 


OK、レイアウトの完成です。 バインディングを終了します。 前にも言ったように、 ViewViewModelインスタンスが必要です。

MainActivity.kt
 class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding var mainViewModel = MainViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = mainViewModel binding.executePendingBindings() } } 


最後に、実行できます


古いデータが 新しいデータに置き換えられていることがわかります

これは単純なMVVMの例です。

しかし、1つの問題があります。画面を変えましょう。


古いデータ新しい データを置き換えました 。 これはどのように可能ですか? アクティビティのライフサイクルを見てください。

アクティビティのライフサイクル
画像

電話をオンにすると、アクティビティの新しいインスタンスが作成され、 onCreate()メソッドが呼び出されました。 私たちの活動を見てください:

MainActivity.kt
 class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding var mainViewModel = MainViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = mainViewModel binding.executePendingBindings() } } 


ご覧のとおり、Activityインスタンスが作成されると、 MainViewModelインスタンス作成されます。 どういうわけか、再作成されたMainActivityごとにMainViewModelの同じインスタンスがあればいいですか?

ライフサイクル対応コンポーネントの概要


なぜなら 多くの開発者がこの問題に直面しているため、Android Frameworkチームの開発者は、この問題を解決するために設計されたライブラリを作成することにしました。 ViewModelクラスはその1つです。 これは、すべてのViewModelが継承するクラスです。

ライフサイクル対応コンポーネントのViewModelからMainViewModelを継承しましょう。 最初に、 ライフサイクル対応コンポーネントライブラリをbuild.gradleファイルに追加する必要があります。

build.gradle
 dependencies { ... implementation "android.arch.lifecycle:runtime:1.0.0-alpha9" implementation "android.arch.lifecycle:extensions:1.0.0-alpha9" kapt "android.arch.lifecycle:compiler:1.0.0-alpha9" 


MainViewModelViewModel 継承者にします

MainViewModel.kt
 package me.mladenrakonjac.modernandroidapp import android.arch.lifecycle.ViewModel class MainViewModel : ViewModel() { ... } 


MainActivityのonCreate()メソッドは次のようになります。

MainActivity.kt
 class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.executePendingBindings() } } 


MainViewModelの新しいインスタンスを作成しなかったことに注意してください。 ViewModelProvidersを使用して取得します。 ViewModelProvidersは、 ViewModelProviderを取得するメソッドを持つUtilityクラスです。 それはすべてscopeに関するものです。 アクティビティでViewModelProviders.of(this)を呼び出すと、 ViewModelは、このアクティビティが生きている限り(再作成せずに破棄されるまで)生き続けます。 したがって、これをフラグメントで呼び出すと、フラグメントが生きている間、 ViewModelは生き続けます。 図を見てください:

スコープのライフサイクル
画像

ViewModelProviderは、最初の呼び出しで新しいインスタンスを作成するか、アクティビティまたはフラグメントが再作成された場合に古いインスタンスを返します。

と混同しないでください

 MainViewModel::class.java 

コトリンでは、従うなら

 MainViewModel::class 

これにより 、JavaのClassとは異なるKClassが返されます。 .javaを記述すると、ドキュメントによると:
KClassのこのインスタンスに対応するJava クラスのインスタンスを返します
画面を回転させるとどうなるか見てみましょう


画面の回転前と同じデータがあります。

前回の記事で、アプリケーションがGithubリポジトリのリストを取得して表示すると述べました。 これを行うには、リポジトリの偽のリストを返すgetRepositories関数を追加する必要があります。

RepoModel.kt
 class RepoModel { fun refreshData(onDataReadyCallback: OnDataReadyCallback) { Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000) } fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First", "Owner 1", 100 , false)) arrayList.add(Repository("Second", "Owner 2", 30 , true)) arrayList.add(Repository("Third", "Owner 3", 430 , false)) Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) },2000) } } interface OnDataReadyCallback { fun onDataReady(data : String) } interface OnRepositoryReadyCallback { fun onDataReady(data : ArrayList<Repository>) } 


RepoModelからgetRepositoriesを呼び出すMainViewModelのメソッドも必要です

MainViewModel.kt
 class MainViewModel : ViewModel() { ... var repositories = ArrayList<Repository>() fun refresh(){ ... } fun loadRepositories(){ isLoading.set(true) repoModel.getRepositories(object : OnRepositoryReadyCallback{ override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories = data } }) } } 


最後に、RecyclerViewでこれらのリポジトリを表示する必要があります。 これを行うには、以下を行う必要があります。


rv_item_repository.xmlを作成するには、CardViewライブラリを使用したため、build.gradle(アプリ)に追加する必要があります。

 implementation 'com.android.support:cardview-v7:26.0.1' 

これは次のようなものです。

rv_item_repository.xml
 <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="android.view.View" /> <variable name="repository" type="me.mladenrakonjac.modernandroidapp.uimodels.Repository" /> </data> <android.support.v7.widget.CardView android:layout_width="match_parent" android:layout_height="96dp" android:layout_margin="8dp"> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/repository_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:text="@{repository.repositoryName}" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.083" tools:text="Modern Android App" /> <TextView android:id="@+id/repository_has_issues" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="@string/has_issues" android:textStyle="bold" android:visibility="@{repository.hasIssues ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toBottomOf="@+id/repository_name" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toEndOf="@+id/repository_name" app:layout_constraintTop_toTopOf="@+id/repository_name" app:layout_constraintVertical_bias="1.0" /> <TextView android:id="@+id/repository_owner" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:text="@{repository.repositoryOwner}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/repository_name" app:layout_constraintVertical_bias="0.0" tools:text="Mladen Rakonjac" /> <TextView android:id="@+id/number_of_starts" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="@{String.valueOf(repository.numberOfStars)}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/repository_owner" app:layout_constraintVertical_bias="0.0" tools:text="0 stars" /> </android.support.constraint.ConstraintLayout> </android.support.v7.widget.CardView> </layout> 


次のステップは、RecyclerViewをactivity_main.xmlに追加することです。 これを行う前に、RecyclerViewライブラリを追加してください:

 implementation 'com.android.support:recyclerview-v7:26.0.1' 

activity_main.xml
 <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="android.view.View"/> <variable name="viewModel" type="me.fleka.modernandroidapp.MainViewModel" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context="me.fleka.modernandroidapp.MainActivity"> <ProgressBar android:id="@+id/loading" android:layout_width="48dp" android:layout_height="48dp" android:indeterminate="true" android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toTopOf="@+id/refresh_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <android.support.v7.widget.RecyclerView android:id="@+id/repository_rv" android:layout_width="0dp" android:layout_height="0dp" android:indeterminate="true" android:visibility="@{viewModel.isLoading ? View.GONE : View.VISIBLE}" app:layout_constraintBottom_toTopOf="@+id/refresh_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:listitem="@layout/rv_item_repository" /> <Button android:id="@+id/refresh_button" android:layout_width="160dp" android:layout_height="40dp" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:onClick="@{() -> viewModel.loadRepositories()}" android:clickable="@{viewModel.isLoading ? false : true}" android:text="Refresh" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="1.0" /> </android.support.constraint.ConstraintLayout> </layout> 



一部のTextView要素を削除し、ボタンがrefreshではなくloadRepositoriesを起動することに注意してください。

button.xml
 <Button android:id="@+id/refresh_button" android:onClick="@{() -> viewModel.loadRepositories()}" ... /> 


不要な場合は、 MainViewModelからrefreshメソッドを、 RepoModelからrefreshDataを削除しましょう。

次に、RecyclerViewのアダプターを作成する必要があります。

RepositoryRecyclerViewAdapter.kt
 class RepositoryRecyclerViewAdapter(private var items: ArrayList<Repository>, private var listener: OnItemClickListener) : RecyclerView.Adapter<RepositoryRecyclerViewAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder { val layoutInflater = LayoutInflater.from(parent?.context) val binding = RvItemRepositoryBinding.inflate(layoutInflater, parent, false) return ViewHolder(binding) } override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position], listener) override fun getItemCount(): Int = items.size interface OnItemClickListener { fun onItemClick(position: Int) } class ViewHolder(private var binding: RvItemRepositoryBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(repo: Repository, listener: OnItemClickListener?) { binding.repository = repo if (listener != null) { binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) }) } binding.executePendingBindings() } } } 


ViewHolderはViewではなくRvItemRepositoryBinding型のインスタンスを取るため、各要素のViewHolderにデータバインディングを実装できることに注意してください。 単一行機能(1行)に困らないでください。

 override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position], listener) 

これは、以下の短いエントリーです。

 override fun onBindViewHolder(holder: ViewHolder, position: Int){ return holder.bind(items[position], listener) } 

そして、 items [position]はインデックス演算子の実装です。 items.get(position)に似ています。

あなたを混乱させるかもしれない別の行:

 binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) }) 

使用していない場合は、パラメーターを_に置き換えることができます。 いいですね

アダプタを作成しましたが、 MainActivityrecyclerViewにはまだ適用していません。

MainActivity.kt
 class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener { lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.viewModel = viewModel binding.executePendingBindings() binding.repositoryRv.layoutManager = LinearLayoutManager(this) binding.repositoryRv.adapter = RepositoryRecyclerViewAdapter(viewModel.repositories, this) } override fun onItemClick(position: Int) { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } } 


アプリケーションを実行する


これは変です。 どうしたの?


したがって、 MainViewModelMainActivityに新しいアイテムを通知する必要があるため、 notifyDataSetChangedを呼び出すことができますか?

できません。

これは本当に重要です。MainViewModelはMainActivityをまったく知らないはずです。

MainActivityMainViewModelのインスタンスを持っているため、変更をリッスンし、 アダプターに変更を通知する必要があります。

しかし、それを行う方法は?

リポジトリーを観察できるため、データを変更した後、アダプターを変更できます。

この決定の何が問題になっていますか?

次のケースを見てみましょう。


まあ、私たちの決定は十分ではありません。

LiveDataの概要


LiveDataは、 ライフサイクルを認識する別のコンポーネントであり 、Viewライフサイクルについて知っているオブザーバブルに基づいています。 そのため、構成の変更によりActivityが破棄され場合、 LiveDataはそれを認識しているため、破棄されたActivityからオブザーバーも削除します。

MainViewModelで実装します

MainViewModel.kt
 class MainViewModel : ViewModel() { var repoModel: RepoModel = RepoModel() val text = ObservableField("old data") val isLoading = ObservableField(false) var repositories = MutableLiveData<ArrayList<Repository>>() fun loadRepositories() { isLoading.set(true) repoModel.getRepositories(object : OnRepositoryReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories.value = data } }) } } 


MainActivityの監視を開始します。

MainActivity.kt
 class MainActivity : LifecycleActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener { private lateinit var binding: ActivityMainBinding private val repositoryRecyclerViewAdapter = RepositoryRecyclerViewAdapter(arrayListOf(), this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.viewModel = viewModel binding.executePendingBindings() binding.repositoryRv.layoutManager = LinearLayoutManager(this) binding.repositoryRv.adapter = repositoryRecyclerViewAdapter viewModel.repositories.observe(this, Observer<ArrayList<Repository>> { it?.let{ repositoryRecyclerViewAdapter.replaceData(it)} }) } override fun onItemClick(position: Int) { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } } 


それはどういう意味ですか? 関数にパラメーターが1つしかない場合、このパラメーターへのアクセスは、itキーワードを使用して取得できます。 したがって、2を掛けるラムダ式があるとします。

 ((a) -> 2 * a) 

次のように置き換えることができます。

 (it * 2) 

今すぐアプリケーションを起動すると、すべてが機能することを確認できます。


...

なぜMVPよりもMVVMを好むのですか?




...

リポジトリパターン


スキーム
画像

先ほど言ったように、 Modelはデータを準備しているレイヤーの単なる抽象名です。 通常、リポジトリとデータクラスが含まれます。 各エンティティ(データ)クラスには、対応するリポジトリクラスがあります。 たとえば、 UserクラスとPostクラスがある場合、 UserRepositoryPostRepositoryも必要です 。 すべてのデータはそこから取得されます。 ViewまたはViewModelからShared PreferencesまたはDBのインスタンスを呼び出さないでください。

そのため、RepoModelの名前をGitRepoRepositoryに変更できます。GitRepoはGithubリポジトリから取得し、Repositoryはリポジトリパターンから取得します。

RepoRepositories.kt
 class GitRepoRepository { fun getGitRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First", "Owner 1", 100, false)) arrayList.add(Repository("Second", "Owner 2", 30, true)) arrayList.add(Repository("Third", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) }, 2000) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) } 


わかりまし。MainViewModelGitRepoRepsitoriesからリポジトリのGithubリストを取得しますが、 GitRepoRepositoriesはどこから取得ますか?

インスタンスからリポジトリでクライアントまたはDBを直接呼び出すことができますが、これはまだベストプラクティスではありません。 アプリケーションはできるだけモジュール化する必要があります。 VolleyをRetrofitに置き換えるために別のクライアントを使用することにした場合はどうなりますか? 内部に何らかのロジックがある場合、リファクタリングを行うのは困難です。 リポジトリは、リモートデータの取得に使用しているクライアントを知る必要はありません。


Androidでの開発を始めたばかりの頃、アプリケーションがオフラインでどのように機能し、データ同期がどのように機能するのかと思っていました。 優れたアプリケーションアーキテクチャにより、これを簡単に行うことができます。 たとえば、インターネット接続がある場合にViewModelの loadRepositoriesが呼び出されると、 GitRepoRepositoriesはリモートデータソースからデータを受信し、ローカルデータソースに保存できます。 電話がオフラインのとき、 GitRepoRepositoryはローカルストレージからデータを受信できます。 そのため、 リポジトリにRemoteDataSourceおよびLocalDataSourceのインスタンスと、このデータの取得であるロジック処理が必要です。

ローカルデータソースを追加します

GitRepoLocalDataSource.kt
 class GitRepoLocalDataSource { fun getRepositories(onRepositoryReadyCallback: OnRepoLocalReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First From Local", "Owner 1", 100, false)) arrayList.add(Repository("Second From Local", "Owner 2", 30, true)) arrayList.add(Repository("Third From Local", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onLocalDataReady(arrayList) }, 2000) } fun saveRepositories(arrayList: ArrayList<Repository>){ //todo save repositories in DB } } interface OnRepoLocalReadyCallback { fun onLocalDataReady(data: ArrayList<Repository>) } 


ここには2つの方法があります。1つ目は偽のローカルデータを返す方法で、2つ目は架空のデータストレージ用です。

リモートデータソースを追加します

GitRepoRemoteDataSource.kt
 class GitRepoRemoteDataSource { fun getRepositories(onRepositoryReadyCallback: OnRepoRemoteReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First from remote", "Owner 1", 100, false)) arrayList.add(Repository("Second from remote", "Owner 2", 30, true)) arrayList.add(Repository("Third from remote", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onRemoteDataReady(arrayList) }, 2000) } } interface OnRepoRemoteReadyCallback { fun onRemoteDataReady(data: ArrayList<Repository>) } 


偽のリモートデータを返すメソッドは1つだけです。

これで、リポジトリにいくつかのロジックを追加できます。

GitRepoRepository.kt
 class GitRepoRepository { val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { remoteDataSource.getRepositories( object : OnRepoRemoteReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) } 


したがって、ソースを共有すると、データをローカルに簡単に保存できます。

ネットワークからのデータのみが必要な場合でも、リポジトリテンプレートを使用する必要がありますか? はい これにより、コードのテストが容易になり、他の開発者がコードをよりよく理解できるようになり、より迅速にサポートできます!

...

Androidマネージャーラッパー


GitRepoRepositoryでインターネット接続を確認して、どこからデータをリクエストするかを知りたい場合はどうしますか?ViewModelとModelAndroid関連のコードを配置するべきではないと既に述べたので、この問題にどのように対処しますか?

インターネット接続用のラッパーを作成しましょう。

NetManager.kt(同様のソリューションが他のマネージャー、たとえばNfcManagerに適用されます)
 class NetManager(private var applicationContext: Context) { private var status: Boolean? = false val isConnectedToInternet: Boolean? get() { val conManager = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val ni = conManager.activeNetworkInfo return ni != null && ni.isConnected } } 


このコードは、マニフェストにアクセス許可を追加した場合にのみ機能します。

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

我々は、コンテキスト(持っていない場合しかし、どのように、リポジトリ内のインスタンスを作成するコンテキストザ・を)?コンストラクタでリクエストできます:

GitRepoRepository.kt
 class GitRepoRepository (context: Context){ val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() val netManager = NetManager(context) fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) } 


ViewModelのGitRepoRepositoryの新しいインスタンスの前に作成しました。NetManagerのコンテキストが必要なときに、ViewModelにNetManagerを含めるにはどうすればよいですか?コンテキストを持つLifecycle対応コンポーネントライブラリのAndroidViewModelを使用できますこれはアプリケーションのコンテキストであり、アクティビティではありません。

MainViewModel.kt
 class MainViewModel : AndroidViewModel { constructor(application: Application) : super(application) var gitRepoRepository: GitRepoRepository = GitRepoRepository(NetManager(getApplication())) val text = ObservableField("old data") val isLoading = ObservableField(false) var repositories = MutableLiveData<ArrayList<Repository>>() fun loadRepositories() { isLoading.set(true) gitRepoRepository.getRepositories(object : OnRepositoryReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories.value = data } }) } } 


この行で

 constructor(application: Application) : super(application) 

MainViewModelのコンストラクターを定義しましたAndroidViewModelコンストラクターでアプリケーションのインスタンスを要求するため、これが必要です。そのため、コンストラクターでは、スーパービューメソッドを呼び出します。このメソッドは、AndroidViewModelコンストラクターを呼び出し、そこから継承します。

注:次の場合、1行を削除できます。

 class MainViewModel(application: Application) : AndroidViewModel(application) { ... } 

そして、GitRepoRepositoryに NetManagerインスタンスがあるので、インターネット接続を確認できます。

GitRepoRepository.kt
 class GitRepoRepository(val netManager: NetManager) { val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { netManager.isConnectedToInternet?.let { if (it) { remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback { override fun onRemoteDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } else { localDataSource.getRepositories(object : OnRepoLocalReadyCallback { override fun onLocalDataReady(data: ArrayList<Repository>) { onRepositoryReadyCallback.onDataReady(data) } }) } } } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) } 


したがって、インターネットに接続している場合、削除されたデータを受信して​​ローカルに保存します。インターネットに接続していない場合は、ローカルデータを取得します。

Kotlinの注意:オペレータが聞かせてヌルのためのチェックをし、内の値を返すIT

次のいずれかの記事で、依存性注入、ViewModelでリポジトリインスタンスを作成するのがいかに悪いか、AndroidViewModelの使用を避ける方法について説明します。また、現在コードにある多くの問題についても説明します。理由のためにそれらを残しました...

これらのライブラリがすべて人気がある理由とそれらを使用する理由を理解できるように、問題を表示しようとしています。

PS私は、マッパー(についての私の心を変えたマッパー)。これについては、次の記事で説明することにしました。

Source: https://habr.com/ru/post/J432826/


All Articles