こんにちは、Habr! Mladen Rakonjacによる記事「
Kotlinによる最新のAndroid開発(パート2) 」の翻訳を紹介します。
ご注意 この記事は、 Mladen Rakonjacの一連の記事の翻訳です(記事日付: 2017年9月23日)。 Github SemperPeritusの最初の部分を読み始めたので、残りの部分が何らかの理由で翻訳されていないことを発見しました。 したがって、私はあなたの注意に第二部をもたらします。 記事は膨大であることが判明しました。
「Android Studio 3.0でAndroidの開発のすべてをカバーするプロジェクトを見つけるのは非常に難しいので、私はそれを書くことにしました。」
この記事では、以下を分析します。
- Android Studio 3ベータ1 パート1
- Kotlinプログラミング言語パート1
- ビルドオプションパート1
- ConstraintLayout パート1
- データバインディングライブラリパート1
- MVVMアーキテクチャ+リポジトリ+ Androidマネージャーラッパーパターン
- RxJava2とそれがパート3アーキテクチャでどのように役立つか
- Dagger 2.11、依存性注入とは何ですか、このパート4を使用する理由
- レトロフィット(Rx Java2を使用)
- ルーム(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として拡張される
アーキテクチャパターンです。 この名前は開発者を混乱させると思います。 私が彼の名前を思いついたのであれば、
ViewModelは
Viewと
Modelを接続しているため、
ViewModelを View-ViewModel-Modelと呼びます。
ビューは、
アクティビティ 、
フラグメント、またはその他のカスタムビュー(
Androidカスタムビュー )の抽象化です。 この
ビューとAndroidビューを混同しないことが重要です。
ビューは馬鹿げているべきであり、それにロジックを書くべきではありません。
ビューにデータを含めることはできません。
ViewModelインスタンスへの参照と、
Viewが必要とするすべてのデータをそこから格納する必要があります。 さらに、
ビューはこのデータを監視する必要があり、
ViewModelのデータが変更されるとレイアウトが変更される必要があります。 要約すると、
Viewは次の役割を果たします。さまざまなデータと状態のレイアウトビュー。
ViewModelは、データとロジックを含むクラスの抽象名です。このデータを受信するタイミングと表示するタイミングです。
ViewModelは現在の
状態を保存し
ます 。
ViewModelは、1つ以上の
Modelへのリンクも保存し、それらからすべてのデータを受け取ります。 たとえば、データがどこから来たのか、データベースから来たのか、サーバーから来たのかを知るべきではありません。 さらに、
ViewModelは
Viewについて何も知る必要がありません。 さらに、
ViewModelはAndroidフレームワークについて何も知らないはずです。Modelは、
ViewModelのデータを準備するレイヤーの抽象名です。 これは、サーバーからデータを受信してキャッシュするか、ローカルデータベースに保存するクラスです。 これらは、User、Car、Square、単純にデータを保存する他のモデルクラスと同じクラスではないことに注意してください。 原則として、これはリポジトリテンプレートの実装であり、後で検討します。
モデルは
ViewModelについて何も知らないはずです。
MVVMは 、正しく実装されていれば、コードを破壊してテストしやすくする優れた方法です。 これにより、
SOLID原則に従うことができるため、コードの保守が容易になります。
コード例
次に、これがどのように機能するかを示す簡単な例を作成します。
始めるために、行を返す簡単な
モデルを作成しましょう:
RepoModel.ktclass 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
onDataReady
、
OnDataReadyCallback
インターフェイスを実装するクラスで
onDataReady
メソッドが呼び出されます。
ViewModelを作成しましょう:
MainViewModel.kt class MainViewModel { var repoModel: RepoModel = RepoModel() var text: String = "" var isLoading: Boolean = false }
ご覧の
RepoModel
、
text
される
RepoModel
、
text
インスタンス、および現在の状態を格納する変数
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
実装をとる
RepoModel
で
refreshData
を呼び出します。 わかりましたが、
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
を呼び出すときは、ビューを
読み込み状態に変更し、データが到着し
isLoading
を
false
に設定する
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>
次に:
- TextViewを変更して、MainViewModelからのテキストを監視します
- isLoading trueの場合にのみ表示されるProgressBarを追加します
- [追加]ボタン。クリックすると、 MainViewModelからrefreshメソッドが呼び出され、 isLoading falseの場合にのみクリック可能になります。
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 imported
、
View.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、レイアウトの完成です。 バインディングを終了します。 前にも言ったように、
View
は
ViewModel
インスタンスが必要です。
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"
MainViewModelを
ViewModel の継承者にし
ます 。
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を作成します
- レイアウトactivity_main.xmlにRecyclerViewを追加します
- RepositoryRecyclerViewAdapterを作成する
- 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) })
使用していない場合は、パラメーターを_に置き換えることができます。 いいですね
アダプタを作成しましたが、
MainActivityの
recyclerViewにはまだ適用していません。
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")
これは変です。 どうしたの?
- アクティビティが作成されたため、事実上空のリポジトリで新しいアダプタも作成されました
- ボタンを押す
- loadRepositoriesを呼び出し、進行状況を表示
- 2秒後、リポジトリが取得され、進行状況は非表示になりますが、表示されません。 これは、 notifyDataSetChangedがアダプターで呼び出されないためです。
- 画面を回転させると、新しいアクティビティが作成されます。そのため、 repositoriesパラメーターとデータを含む新しいアダプターが作成されます
したがって、
MainViewModelは
MainActivityに新しいアイテムを通知する必要があるため、
notifyDataSetChangedを呼び出すことができますか?
できません。
これは本当に重要です
。MainViewModelはMainActivityをまったく知らないはずです。
MainActivityは
MainViewModelのインスタンスを持っているため、変更をリッスンし、
アダプターに変更を通知する必要があります。
しかし、それを行う方法は?
リポジトリーを観察できるため、データを変更した後、アダプターを変更できます。
この決定の何が問題になっていますか?
次のケースを見てみましょう。
- MainActivityでは 、リポジトリを観察します。変更が発生すると、 notifyDataSetChangedを実行します
- ボタンを押す
- データの変更を待っている間に、 構成の変更によりMainActivityが再作成される場合があります。
- MainViewModelはまだ生きています
- 2秒後、 リポジトリフィールドは新しいアイテムを受け取り、データが変更されたことをオブザーバーに通知します
- オブザーバーは、もう存在しないアダプターでnotifyDataSetChangedを実行しようとします。 MainActivityが再作成されました
まあ、私たちの決定は十分ではありません。
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")
それはどういう意味ですか? 関数にパラメーターが1つしかない場合、このパラメーターへのアクセスは、itキーワードを使用して取得できます。 したがって、2を掛けるラムダ式があるとします。
((a) -> 2 * a)
次のように置き換えることができます。
(it * 2)
今すぐアプリケーションを起動すると、すべてが機能することを確認できます。
...
なぜMVPよりもMVVMを好むのですか?
- Viewには退屈なインターフェイスはありません。 ViewModelにはViewへの参照がありません
- Presenterには退屈なインターフェイスはありません。これは必要ありません
- 構成変更の処理がはるかに簡単
- MVVMを使用すると、アクティビティ、フラグメントなどのコードが少なくなります。
...
リポジトリパターン
先ほど言ったように、
Modelはデータを準備しているレイヤーの単なる抽象名です。 通常、リポジトリとデータクラスが含まれます。 各エンティティ(データ)クラスには、対応する
リポジトリクラスがあります。 たとえば、
Userクラスと
Postクラスがある場合、
UserRepositoryと
PostRepositoryも必要
です 。 すべてのデータはそこから取得されます。 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>) }
わかりました
。MainViewModelは
GitRepoRepsitoriesからリポジトリの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>){
ここには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とModelにAndroid関連のコードを配置するべきではないと既に述べたので、この問題にどのように対処しますか?インターネット接続用のラッパーを作成しましょう。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私は、マッパー(についての私の心を変えたマッパー)。これについては、次の記事で説明することにしました。