この記事では、Androidアプリのアーキテクチャについて、改めて纏めています。
ポイント
Androidに関わらず、オブジェクト思考ではよく言われる、下記を守ることです。
- クラスをシンプルに
- 依存を少なく
技術要素
ここでは、下記の技術要素を使用しています。
- Androidの推奨アーキテクチャ
https://developer.android.com/jetpack/guide?hl=ja#recommended-app-arch - データバインディング
https://developer.android.com/topic/libraries/data-binding?hl=ja - AndroidでのDI
https://developer.android.com/training/dependency-injection?hl=ja - DataStore
https://developer.android.com/topic/libraries/architecture/datastore?hl=ja
実装テーマ
- メモ画面。
- 保存先は、DataStore。
クラス構成
構成は、下記を参考に、MemoFragment、MemoViewModel、MemoRepositoryです。
依存方向:MemoFragment → MemoViewModel → MemoRepository
実装内容
MemoFragment
ポイントはDataBinding。
DataBindingで、UI(xml)とソース(ここでは、ViewModel)を繋ぎます。
class MemoFragment : Fragment() {
private lateinit var binding: DataStoreFragmentBinding
private val viewModel: MemoViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.data_store_fragment, container, false)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
return binding.root
}
...
DataBindingを使うために、app/build.gradleに下記を追加します。
dataBinding {
enabled = true
}
layoutファイルは、layout階層を追加します。
dataには、MemoViewModelを追加します。
EditTextにandroid:text="@={viewModel.text}"
を記載して、MemoViewModelのLiveDataと双方向にバインドさせます。(双方向のポイントは@と{の間の=)
Buttonには、android:onClick="@{() -> viewModel.onClick()}"
を記載して、クリック時のメソッドをバインドします。
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="com.ykato.sample.kotlin.datastore.MemoViewModel" />
</data>
...
<EditText
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="top"
android:text="@={viewModel.text}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/guideline" />
...
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/save_button"
android:onClick="@{() -> viewModel.onClick()}"/>
...
MemoViewModel
ViewModelはDI(Dependency Injection)を使用することで、依存度を下げます。
MemoRepositoryをメンバ変数として保持する場合、コンストラクタの引数としてInjectionすることで依存が下がります。(Mockもしやすく、テストもしやすくなります。)
@HiltViewModel
class MemoViewModel @Inject constructor(
private val store: MemoRepository
): ViewModel() {
...
DI(Hilt, Dagger)を行うために、依存関係をgradleファイルに追加します。
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
...
}
}
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
...
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
...
}
...
dependencies {
implementation "com.google.dagger:hilt-android:2.35"
kapt "com.google.dagger:hilt-android-compiler:2.35"
}
...
また、DIを行う為には、Applicationクラスに@HiltAndroidApp
を追加する必要があります。
@HiltAndroidApp
class KotlinSampleApplication: Application()
MemoFragmentとMainActivityには@AndroidEntryPoint
を追加する必要があります。
(MemoFragmentをMainActivity上に追加しているため、MainActivityにも追加する必要がある。)
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
...
@AndroidEntryPoint
class MemoFragment : Fragment() {
...
MemoRepository
MemoRepositoryはDataStoreを使用して、メモの内容を保存します。
また、メモの内容をキャッシュする(DataStoreの読み込み・保存は非同期処理なので、キャッシュして同期メソッドを実装する)ため、Singletonにしています。
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "memo")
val MEMO_STRING = stringPreferencesKey("memo_string")
@Singleton
class MemoRepository @Inject constructor(
@ApplicationContext private val context: Context,
) {
private val cache by lazy { AtomicReference<String?>(null) }
...
DataStoreを使用するために、依存に下記を追加
...
dependencies {
implementation("androidx.datastore:datastore-preferences:1.0.0-rc01")
...