- 投稿日:2021-01-11T22:52:30+09:00
Android 学習記録 リポジトリパターン
目的
ここ半年、サーバサイドの開発ばかりやっていてAndroidの開発やってないなぁと思い、リハビリがてら勉強しようと思いました。
リポジトリパターンを使った実装というのをやったことがないので、試しにやってみたいと思います。
自分のための勉強メモとしての意味が強いので、他人から見たらよくわからないかもです。すみません。私のAndorid開発のレベル
開発期間
- 1年ちょいくらい
言語
- Java(業務で使用)
- Kotlin(プライベートで少し触ったくらい)
知ってること
- Androidの基礎知識は大体。
- アーキテクチャはMVVMだけ(他にあまりメリットを感じない)。
- Retrofit+RxJavaは多少経験あり。
知らない事
- 割とモダンなこと(流行とか)は知らない
- リポジトリパターンも概念しか知らない
- Dagger(AndroidというよりJava?)も使ったことない。
学習サイト
以下のリンク先で勉強!
https://developer.android.com/courses/kotlin-android-fundamentals/overview
https://developer.android.com/codelabs/kotlin-android-training-repository?index=..%2F..android-kotlin-fundamentals#02021年1月時点での記録なので、ご了承ください。
環境
Windows10
AndroidStudio 3.6.3
でやってみたところ、* Exception is: com.intellij.openapi.externalSystem.model.ExternalSystemException: This version of Android Studio cannot open this project, please retry with Android Studio 4.0 or newer.ふ~~ん...
半年前くらいにインストールしたAndroidStudioでは古くて動きませんでした。
というわけでバージョンアップ。参考:バージョンアップ方法
AndroidStudioのバージョンアップ簡単で良いですね。Androidの技術サイクルは早いなぁ~と感心しつつ
AndroidStudio 4.1.1
にバージョンアップ完了!学習記録
ソースコードのウォークスルー
起動
起動ボタンを押したところ
Failed to install the following Android SDK packages as some licences have not been accepted. build-tools;29.0.2 Android SDK Build-Tools 29.0.2AndroidStudioのバージョンといい、あんまり動かなくて挫けそうになりましたが、[Tools]->[SDK Manager]からSDK(APIレベル29)を追加。
(いきなり学習サイトの途中から始めてますが、本当は前提の環境が決まってるんですかね...?)ということで、もう一度リトライ!
あれ、もう完成されてる...
本当はネットワークエラーが起きるところから始まるっぽいのですが、もういいや感があったので続行します。
(ちゃんとstarterプロジェクトからGitで落としたんですけどね...)ネットワーク遅延
ネットワーク遅延したときに、ローディングマークが出ることを確認します。
これはリポジトリパターンとは関係なく、おまけ的な確認のようです。
以下にdelay(2000)を追加しますprivate fun refreshDataFromNetwork() = viewModelScope.launch { try { // 遅延処理を追加(サンプルコードはcatch句に書いているが、正常動作しているのでこっちに書く) delay(2000) val playlist = DevByteNetwork.devbytes.getPlaylist() _playlist.postValue(playlist.asDomainModel()) _eventNetworkError.value = false _isNetworkErrorShown.value = false } catch (networkError: IOException) { // Show a Toast error message and hide the progress bar. _eventNetworkError.value = true } }viewModelScope.launchとすると、コルーチンが起動するみたいです。
delayは多分コルーチンの処理を止める関数。コルーチンについて詳しくないので別途勉強が必要ですが、とりあえず非同期処理してるんだな、ぐらいでここは流します。
ちなみにネットワークアクセスのような処理はUIスレッド(メインスレッド)でやると、画面が止まってしまうので、バックグラウンドで非同期で動かす必要があります。
実行結果
ぐるぐるマークが出るようになりました。
ドメインパッケージ
domain/Models.ktdata class DevByteVideo(val title: String, val description: String, val url: String, val updated: String, val thumbnail: String) { val shortDescription: String get() = description.smartTruncate(200) }kotlinではdata classと書くと、get/setterを省略したBeanクラスを作れるみたいです。(最高だぁ~)
このDevByteVideoクラスが、外部システムからとってきた情報を格納する構造体の役割になりそうです。ネットワークパッケージ
その名の通り、DTO的なクラスがたくさんあります。
ただのDTOなのでほぼ省略しますが、以下の書き方が参考になったので転記します。network/DataTransferObjects.ktfun NetworkVideoContainer.asDomainModel(): List<DevByteVideo> { return videos.map { DevByteVideo( title = it.title, description = it.description, url = it.url, updated = it.updated, thumbnail = it.thumbnail) } }JavaのStreamAPIもこんな感じで書けるんですね。
network/Service.kt
こちらはDAOっぽいクラスです。
Retrofitを使ったHTTPアクセスを行います。
特筆することは特にないので流しますが、UIはLiveDataとReciclerViewを使って描画しますよ~的なことが、学習サイトに書いてありました。オフラインキャッシュ
今回はHTTPアクセスでデータを取得した後、オフラインキャッシュする設計のようです。
もしかしたら、Androidではこれが一般的なのかもしれません。キャッシュされたデータを使うことで、同じデータに複数回アクセスするときにHTTPアクセスが不要(HTTP通信は結構時間かかるので)になります。
キャッシュの実現方法にはいろいろありますが、今回はRoomを使います。
RoomはAndroid端末内のローカルDB(Sqlite3)を使うためのライブラリです。要はローカルDBでデータを永続化しますってことですね。
Roomの依存関係の追加
特筆事項はないですが、一応転記しておきます。
// Room dependency def room_version = "2.1.0-alpha06" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version"データベースオブジェクトの作成
ここではRoom用のDTO的なのを作ります。
database/DatabaseEntities.kt@Entity data class DatabaseVideo constructor( @PrimaryKey val url: String, val updated: String, val title: String, val description: String, val thumbnail: String) /** * データベースのDTOからドメインのDTOに変換 */ fun List<DatabaseVideo>.asDomainModel(): List<DevByteVideo> { return map { DevByteVideo( url = it.url, title = it.title, description = it.description, updated = it.updated, thumbnail = it.thumbnail) } }@EntityのアノテーションはRoomで使うDTOの目印です。
@PrimaryKeyはその名の通りPKに付けます。あと、List.asDomainModel()の書き方ですが、Kotlinには拡張関数という便利機能があって、継承とかしなくても関数を拡張できるようです。
要はリストインタフェースにasDomainModel()という関数を拡張して使えるようにしたということですね。
ここで、HTTPアクセス側のDTOも拡張関数を追加します。
network/DataTransferObjects.kt/** * HTTPアクセスのDTOからデータベースのDTOに変換 */ fun NetworkVideoContainer.asDatabaseModel(): List<DatabaseVideo> { return videos.map { DatabaseVideo( title = it.title, description = it.description, url = it.url, updated = it.updated, thumbnail = it.thumbnail) } }Roomの実装
database/Room.kt@Dao interface VideoDao { @Query("select * from databasevideo") fun getVideos(): LiveData<List<DatabaseVideo>> @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll( videos: List<DatabaseVideo>) } @Database(entities = [DatabaseVideo::class], version = 1) abstract class VideosDatabase: RoomDatabase() { abstract val videoDao: VideoDao } private lateinit var INSTANCE: VideosDatabase fun getDatabase(context: Context): VideosDatabase { synchronized(VideosDatabase::class.java) { if (!::INSTANCE.isInitialized) { INSTANCE = Room.databaseBuilder(context.applicationContext, VideosDatabase::class.java, "videos").build() } } return INSTANCE }Dao
@Daoで、RoomのDaoであることの目印を付けます。
いわゆるORMってやつで、@QueryでSELECT文の結果をリターンし、@Insertで@Entityの内容をINSERTするようです。
onConflictでPK被ったときにどうするかを指定できるようで、今回はREPLACEにしています。(おそらくSQLがREPLACE文になる?)Database
@DatabaseでDatabaseとしての目印を付けます。
entitiesで@Entityを指定します。
versionについてはよくわかりませんが、DBをマイグレーションするためのバージョン管理って感じでしょうか。いまいち仕組みわかってませんが、ここは一旦流します。
あと、クラスはRoomDatabaseを継承する必要がありそうです。Kotlinは、abstract val で変数を抽象化できるようです。(Javaにもあったっけ?)
多分VideoDaoはDIコンテナみたいな機能で、@Daoアノテーションのインスタンスが勝手に生成されるっぽいです。その他
database/Room.ktprivate lateinit var INSTANCE: VideosDatabase fun getDatabase(context: Context): VideosDatabase { synchronized(VideosDatabase::class.java) { if (!::INSTANCE.isInitialized) { INSTANCE = Room.databaseBuilder(context.applicationContext, VideosDatabase::class.java, "videos").build() } } return INSTANCE }いろいろメモりたいことがあります。
まず、lateinitとは、遅延初期化です。kotlinは基本的にnullを許しませんが、lateinitで遅延初期化を許します。あと、サラッとトップレベルに関数とか変数とか定義してます。Kotlinではトップレベルに定義できるみたいです。「::INSTANCE」は軽く調べても出てこなかったのですが、トップレベルでのメソッド参照だと思います。
要はシングルトンで、VideosDatabaseインスタンスを作成する機能をここで実現しています。
ここまでが、Roomを使ったオフラインキャッシュの仕組みです。
ここからがリポジトリパターンを適用していくフェーズです。(前置きが長かった)リポジトリパターン
概要図
要は、Repositoryを仲介させて、ViewModelが直接DBやHTTPのような外部へのアクセスをしないようにする設計です。
今回は、オンラインでデータ取得する処理とオフラインキャッシュからデータを取得する処理があります。
その処理の実現方法は、アプリケーションのメイン処理(ロジック)からすれば、何でもよい(=依存させたくない)ので、Repositoryを仲介させる、というのが多分大きな目的のようです。あと、Repositoryに限らず、外部へのアクセスのモジュールは、昨今、技術の移り変わりが激しく、変更のかかりやすい部分です。
また、大事なロジックを記述しているモジュールが外部に依存すると、当然テストも外部に依存したテストになってしまいます。ということで、クリーンアーキテクチャを筆頭に、昨今では大事なロジック部分は、外部に依存させないのが主流な気がします。(クリーンアーキテクチャ的にいうと、この設計だと依存の向きが良くない気がしますが、リポジトリパターンの範囲外ということでしょうか...?)
Repositoryの実装
repository/VideosRepository.ktclass VideosRepository(private val database: VideosDatabase) { suspend fun refreshVideos() { withContext(Dispatchers.IO) { Timber.d("refresh videos is called") // Retrofit(Rest ClientのGetメソッド)を使用して、データ取得 val playlist = DevByteNetwork.devbytes.getPlaylist() // Database用のDTOに変換して、インサート処理 database.videoDao.insertAll(playlist.asDatabaseModel()) } } // DatabaseのDTOから、Domainのオブジェクトに変換 val videos: LiveData<List<DevByteVideo>> = Transformations.map(database.videoDao.getVideos()) { it.asDomainModel() } }refreshVideos()メソッドで、HTTP(REST)でデータ取得して、DBへインサートします。
DBアクセスなので、コルーチンを使った非同期処理です。
suspendとかその辺のお作法はまだよくわかりません。Transformations.map()はLiveDataでラップされたリストの変換メソッドのようです。(Javaにもあるようですが、知りませんでした)
ViewModelの実装(Repositoryの呼び出し)
viewmodels/DevByteViewModel.ktprivate val videosRepository = VideosRepository(getDatabase(application)) val playlist = videosRepository.videos init { refreshDataFromRepository() } private fun refreshDataFromRepository() { viewModelScope.launch { try { videosRepository.refreshVideos() _eventNetworkError.value = false _isNetworkErrorShown.value = false } catch (networkError: IOException) { // Show a Toast error message and hide the progress bar. if(playlist.value.isNullOrEmpty()) _eventNetworkError.value = true } } }Repository呼び出しのときは、コルーチンによる非同期処理です。
あと、playlist.value.isNullOrEmpty() これ、Javaやってる人だと、え?ってなってしまうと思うのですが、isNullOrEmpty()は、JavaのOptionalのようなの動作をする(KotlinのNullable型の拡張関数)らしいです。ということで、完成です。
まとめ
- リポジトリパターンを使う理由
- オフラインキャッシュの仕組みをViewModelから隠蔽したい
- ViewModelから外部アクセス(DBやHTTP)を隠蔽したい
- よくわからない点
- クリーンアーキテクチャを意識するのであれば、ViewModelはRepositoryに依存するべきではないのでは?
- リポジトリ関係ないけど、ViewModelにロジック実装するのって微妙じゃね...?
- 投稿日:2021-01-11T21:42:16+09:00
AndroidのHiltをkotlinで実装する
Hilt は Android 用の依存関係インジェクション ライブラリです。これを使うことで、プロジェクトで依存関係の注入(DI)を手動で行うためのボイラープレートが減ります。Daggerが非常にわかりやすくなっていたので、これからHiltを使ってみましょう。
ライブラリの導入
まず、hilt-android-gradle-plugin プラグインをプロジェクトのルート build.gradle ファイルに追加します。
buildscript { ... dependencies { ... classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha' } }次に、Gradle プラグインを適用し、app/build.gradle ファイルに次の依存関係を追加します。
... apply plugin: 'kotlin-kapt' apply plugin: 'dagger.hilt.android.plugin' android { ... } dependencies { implementation "com.google.dagger:hilt-android:2.28-alpha" kapt "com.google.dagger:hilt-android-compiler:2.28-alpha" }Hilt は Java 8 の機能を使用しています。プロジェクトで Java 8 を有効にするには、app/build.gradle ファイルに次の内容を追加します。
kotlinのプロジェクトの場合はkotlinOptionsの配置を追加する必要です
android { ... compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } // For Kotlin projects kotlinOptions { jvmTarget = "1.8" } }Hiltの一般的なインジェクションの意味
@HiltAndroidApp
- Hilt を使用するアプリには、@HiltAndroidApp アノテーションが付けられた Application クラスが含まれている必要があります。
- @HiltAndroidApp は、Hilt のコード生成をトリガーします。これには、アプリケーション レベルの依存関係コンテナとして機能するアプリケーションの基本クラスも含まれます。
- ここで生成された Hilt コンポーネントは、Application オブジェクトのライフサイクルにアタッチされ、依存関係を提供します。また、アプリの親コンポーネントであることから、他のコンポーネントがこのコンポーネントの提供する依存関係にアクセスできます。
- Hilt はApplication、Activity、Fragment、View、Service、BroadcastReceiverの Android クラスをサポートしています
@HiltAndroidApp class App : Application() { }@AndroidEntryPoint
- Activity:FragmentActivity、AppCompatActivityなどのComponentActivity を拡張するアクティビティのみをサポートします
- Fragment:androidx.Fragment を拡張するフラグメントのみをサポートします。
- View
- Service
- BroadcastReceiver
- Androidクラスに@AndroidEntryPointの注釈が付けられている場合は、依存するAndroidクラスに同じ注釈を追加する必要があります
@AndroidEntryPoint class MainActivity:AppCompatActivity()@Inject
- クラスのコンストラクタで @Inject アノテーションを使用して、そのクラスのインスタンス提供方法を Hilt に知らせます。コンストラクター、非プライベートフィールド、メソッドでよく使用されます
class AnalyticsAdapter @Inject constructor( private val service: AnalyticsService ) { @Inject lateinit var user:User }@Module
@Moduleでアノテーションが付けられたクラスの場合、@ InstallInを使用してモジュールのスコープを指定する必要があります。多くの場合、依存クラスのオブジェクト(サードパーティライブラリOkHttp、Retrofitなど)を作成するために使用されます。
@Module @InstallIn(ApplicationComponent::class) // ApplicationComponentが使用されるため、NetworkModuleはApplicationのライフサイクルにバインドされます。 object NetworkModule { }@Provides
クラスの内部メソッドを@Moduleアノテーションでマークし、依存関係オブジェクトを提供するためによく使用されます。
@Module @InstallIn(ApplicationComponent::class) object NetworkModule { /** * @Provides * @Singleton */ @Provides @Singleton fun provideOkHttpClient(): OkHttpClient { return OkHttpClient.Builder() .build() } }具体的なソースを説明ソースに置きました。是非試してみてください。
- 投稿日:2021-01-11T18:28:04+09:00
Androidアプリ開発における便利なライブラリたち
はじめに
今回は僕がAndroidアプリ開発においてとても便利だと感じたライブラリを紹介していきます。
1. Timber
TimberとはAndroidの通常のLogクラスの上にユーティリティを提供する、小さくて拡張可能なAPIを持つロガーです。
Android界隈では超がつくほど有名なあのJakeWhartonが作ったライブラリです。
通常Andriodにおけるログは
Log.d("debug", "hogehoge")というふうにログを出力しますが、このTimberを使うと
Timber.d("debug: hogehoge"というふうに引数を一つ減らしてログを出力できます。
このTimberはAndroid標準のLogクラスをラップしているらしいので非常に軽量です。またrelease版ではログを出力したくない場合もあると思いますが、それはアプリケーションクラスで
class MyApplication : Application() { override fun onCreate() { super.onCreate() if(BuildConfig.Debug) { Timber.plant(Timber.DebugTree()) } } }としてあげるとrelease版ではログを出力しないでくれます。とても便利ですね。
Treeをplantしてあげるという表現、とてもおしゃれ
2.CircleImageView
ImageViewを丸くしたいということがよくあると思いますが(僕はよくある)、そんな時にCircleImageViewがとても便利です。
https://github.com/hdodenhof/CircleImageView普段はこれを使わないと、drawableを自分で自作してあーだこーだやらなくては行けませんが、このライブラリを使うと簡単に丸いImageViewが使用できます。
使い方は特にImageViewと変わらず、xmlでCircleImageViewを書いてあげるだけ。
オリジナルのプロパティもかなりあり、ボーダーの色、ボーダーの太さ、背景色など自由にカスタマイズできます。
3.Glide
Glideは、画像読み込みライブラリです。
APIなどで受け取ったURLをそのまま使用し、画像を表示できます。一昔前はAsyncTaskを使ったりとかなりだるかったのですが、これを知ってから画像読み込みがかなり楽になりました。
こちらも使い方はとても簡単
val imageView: ImageView = findViewById(R.id.imageView) Glide.with(context).load(url).into(imageView)とするだけ。
ImageViewのBindingAdapterを作ればもっと便利ですね
@BindingAdapter("imageUrl") fun ImageView.setImageUrl(imageUrl: String?) { imageUrl ?: return Glide.with(context).load(imageUrl).into(this) }こんなBindingAdapterを作って、xml側で
<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:imageUrl="@{url}"/>こうするだけ。
あとはライブラリ側で勝手に画像を読み込んで、ImageViewに表示してくれますまとめ
世の中には便利なライブラリがたくさんありますね。
皆さんのおすすめのライブラリがあったらぜひ教えてください。
- 投稿日:2021-01-11T16:54:43+09:00
StateFlowがDataBindingに対応したらしいので、記事などまとめ
以下参考になりそうな記事とか
Android Studio Release Updates: Android Studio Arctic Fox Canary 4 available
DataBindingにStateFlowが対応したよっていう告知StateFlow vs LiveData : android_devs
どう違うの?っていうのを解説してくれてます!ありがとう海外の仲間よ!android - How to replace LiveData with Flow - Stack Overflow
LiveDataをFlowで書き換える実例ですStateFlow y SharedFlow ¿el fin de LiveData? - Speaker Deck
スライドがあったので紹介します。
でもこれスペイン語なんですよね。。。そもそもFlowとかなーにって方へ
kotlin coroutinesのFlow, SharedFlow, StateFlowを整理する - Blog - Mori Atsushi
【Kotlin】SharedFlowとStateFlowを読み解く - Qiita
LiveDataとStateFlowの違い | by Kenji Abe | Medium
kotlin coroutines flowでいいねボタン問題を解決しよう - Blog - Mori Atsushi
まとめ
なんか良さげな情報あったら教えてください!
- 投稿日:2021-01-11T14:26:25+09:00
(Android) DataBindingを使ってテキストを動的に変更する
はじめに
前回は DataBindingを使って静的テキストを表示させてみるを紹介しました。
今回はこれに少し手を加えて、ボタンをクリックするとテキストが切り替わるアプリを実装します。
クリック後、データを変更するためのインターフェースを定義
SampleEventHandlers.javapublic interface SampleEventHandlers { void onChangeClick(View view); }レイアウトにボタンを追加/クリックイベントと紐付ける
activity_main.xml<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Binding Objects --> <data> <variable name="user" type="com.example.databinding2.User" /> <!-- handlersという名前(任意)で、ハンドラー(インターフェース)が設定される --> <variable name="handlers" type="com.example.databinding2.SampleEventHandlers" /> </data> <!-- View --> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.name}" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.email}" /> <Button android:id="@+id/button_change" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Change" android:onClick="@{handlers.onChangeClick}" /> <!-- ボタンとSampleEventHandlers.javaのonChangeClickを紐付ける --> </LinearLayout> </layout>モデルクラスに値を変更するメソッドを追加(未完成 理由は後述)
User.javapublic class User { private String name; private String email; public User(String name, String email) { this.name = name; this.email = email; } public String getName() { return name; } public String getEmail() { return email; } // セッターを追加 public void setName(String name) { this.name = name; } // セッターを追加 public void setEmail(String email) { this.email = email; } }DataBinding処理(クリックイベント)を追加
MainActivity.javapublic class MainActivity extends AppCompatActivity implements SampleEventHandlers{ private User user = new User("Taro", "taro@test.com"); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Bindingのインスタンスを取得 ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main); // xmlのuserとMainActivityのuserを紐付ける binding.setUser(user); // xmlのhandlersにMainActivityのonChangeClick()を紐付ける binding.setHandlers(this); } // buttonをクリックしたときのイベント処理 @Override public void onChangeClick(View view) { if (user.getName().equals("Taro")) { user.setName("Jiro"); user.setEmail("jiro@test.com"); Log.d("DEBUG", user.getName()); } else { user.setName("Taro"); user.setEmail("taro@test.com"); } } }アプリビルドしてみるが。。。
これを実行するとログには"Jiro"と表示されるが、画面に表示される文字は"Taro"のままとなってしまう。なぜ?
どうやらプロパティの変更をViewに通知する仕組みが必要みたいで、オブジェクトを監視できるようにUserモデルを修正する必要があるとのこと。
モデルクラスを修正(プロパティの変更をViewに通知する)
Point
- BaseObservableを継承する
- getメソッドにBindableを追加
- 監視用の定数BR.name,BR.emailが生成できるようになる
- setメソッド内にnotifyPropertyChanged(BR.name)を追加
- レイアウト側からBR.nameに対応するgetName()が呼ばれるようになる
User.javapublic class User extends BaseObservable { private String name; private String email; public User(String name, String email) { this.name = name; this.email = email; } @Bindable public String getName() { return name; } @Bindable public String getEmail() { return email; } public void setName(String name) { this.name = name; notifyPropertyChanged(BR.name); } public void setEmail(String email) { this.email = email; notifyPropertyChanged(BR.email); } }参考サイト
https://qiita.com/Omoti/items/a83910a990e64f4dbdf1#step0-%E5%B0%8E%E5%85%A5
ありがとうございました
- 投稿日:2021-01-11T12:13:30+09:00
FlutterでstatelessなTextFieldを作ったので宣伝したい
はじめに
FlutterやReactにおいて、大きな関心ごとの一つに状態管理がある。
TextFieldはその性質上、自ら状態を持つ設計になりやすい。
しかし、TextFieldの入力情報をcallbackで返し、TextFieldの引数に現在値を与えることでTextFieldが状態を持たないようにできる。
ユースケースはProvider
などの状態管理ツールを入れているプロジェクトに限定されると思うが、私が欲しいと思ったので、今回作成した。
ついでに勉強がてら公開してみた。メリット
- TextFiledの状態を自前で管理できるようになる(Provider, redux, Reverpod, BLoc, etc...)
- 他のwidgetの状態と連動させやすい
デメリット
- 自前で状態管理するコードを書く必要がある
- rebuildのタイミングを把握していないと、文字列入力中にwidgetがrebuildされ、文字が消えることがある
先行記事
Flutterにおける状態管理は様々な手法がある。こちらの記事が非常によくまとまっていると感じた。
この記事では状態管理方法については言及せず、外側で状態管理をしやすいTextFieldについて記述する。こちらの記事と全く目的が同じである。
しかし、こちらの記事では状態管理を完全に外だししているわけではなく、Stateに状態を持たせてしまっているため、外からTextFieldに表示したい文字列を与えても変化しない課題がある。
WidgetとStateの関係についてはこちらの記事がとても参考になった。使い方
こちらのリポジトリに公開している。
また、pub.devにも公開している。exampleを見てもらうのが一番早いが、こちらにもコードを載せておく。
githubやpub.devが最新版となるため、そこはご了承を...class _MyHomePageState extends State<MyHomePage> { String _message = ""; void _setMessage(String str) => setState(() => _message = str); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ StatelessTextField( initialValue: _message, style: Theme.of(context).textTheme.headline4, decoration: InputDecoration(hintText: "first message field"), onSubmitted: _setMessage, ), StatelessTextField( initialValue: _message, style: Theme.of(context).textTheme.headline6, decoration: InputDecoration(hintText: "second message field"), onSubmitted: _setMessage, ) ], ), ), ); } }簡単に説明すると、縦に並ぶ二つの
StatelessTextField
のinitialVlaue
に_message
を与えて、onSubmitted
で_message
を更新してrebuildするexampleである。
このようにStatelessTextField
は外側で状態管理をしたいときに有効である。実装(使うだけなら知らなくてもOK)
TextFieldに初期値を与えるためには、
TextEditController
を保持しなければならないため、やむなくStatefulWidgetをextendsして作成した。
が、使用感はStatelessWidgetなので、stateless_textfield
と命名した。exampleのような構成ではkeyを正しく与えないとWidgetとStateの組み合わせがずれてしまう場合があると思うが、Stateに状態を持たない構成なので、ずれてしまっても問題ない。
なんとなく気持ち悪いと思うが、悪影響はなさそうなので、このような実装にした。TextEditControllerが曲者
TextFieldに初期値を与えるためには
TextEditController
を用意しなければならない。
また、これはちゃんとdispose()
を呼ばないとメモリリークする危険物であるため、そのハンドリングのためにStateで保持している。
逆に言えば、Stateで保持しているのはこれだけである。(たまにちゃんとdisposeしていないサンプルを見かけるが、大丈夫なのだろうか...)最後に
Flutterの勉強を初めて半月くらいですが、WidgetとElementの関係や、公式の提供するWidgetの豊富さが面白く、「Flutter凄いー」となっています。
フロントエンドを例えに出すとReact + Material-UIといった感じですね。サクサク作れます。
Flutterの勉強がてら、自分用のflutter_templateを作っていたところ、状態を持たないTextFieldが欲しくなったので作りました。
- 投稿日:2021-01-11T12:00:32+09:00
改めて「architecture-samples usecase」ブランチからUseCaseの使い方を学んでみる
随分と久々の投稿になってしまいました。
前回の投稿から1年以上経過していますね。今回は、「Android Architecture Blueprints - Use Cases/Interactors in Domain layer」を改めて読み込んでみました。
弊社アプリではオニオンアーキテクチャを採用していますが、
その文脈のDomain(or Application) ServiceにUseCaseを配置しようと試みています。
その参考のため、上記のGithubリポジトリが非常に参考になりそうでした。※本投稿では、オニオンアーキテクチャとそれに関する用語は説明しません...m(_ _)m
UseCaseはどの様に使われているか
結論は、ViewModelのコンストラクタ引数としてインジェクトされています。
class SampleViewModel( private val aaaUseCase: AaaUseCase, private val bbbUseCase: BbbUseCase, private val cccUseCase: CccUseCase ) : ViewModel() { fun loadAaa(forceUpdate: Boolean) { viewModelScope.launch { val result = aaaUseCase(forceUpdate) ... } } }class AaaUseCase( private val xxxRepository: XxxRepository ) { suspend operator fun invoke( forceUpdate: Boolean = false ): YYYY { ... }なるほどですね。
UseCaseはどうやってインスタンス化する?
ViewModelFactoryを使います。
上記サンプルでは、Applicationクラス生成時にRepositoryを生成します。
そして、FragmentでViewModelFactoryを生成する際に、Repositoryをコンストラクタ引数でインジェクトしています。@Suppress("UNCHECKED_CAST") class ViewModelFactory constructor( private val xxxRepository: XxxRepository ) : ViewModelProvider.NewInstanceFactory() { override fun <T : ViewModel> create(modelClass: Class<T>) = with(modelClass) { when { isAssignableFrom(SampleViewModel::class.java) -> SampleViewModel( AaaUseCase(xxxRepository), BbbUseCase(xxxRepository), CccUseCase(xxxRepository) ) else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") } } as T }class SampleFragment : Fragment() { private val viewModel by viewModels<SampleViewModel> { getViewModelFactory() } } fun Fragment.getViewModelFactory(): ViewModelFactory { val repository = (requireContext().applicationContext as MyApplication).xxxRepository return ViewModelFactory(repository) }Repositoryをどうやってインスタンス化するかは、状況によるかと思います。
弊社では、Daggerを活用したServiceProvider的なクラスを用意しています。UseCaseはどのレイヤーに配置する?
Domain (or Application) Serviceに配置しようと思いました。
弊社アプリでは、Repository interfaceをDomain Servicesのレイヤーに配置していますが
同じ階層にUseCaseを配置しようと考えています。Junit testはどう書く?
上記ブランチでは、Repository, UseCase, ViewModel, Fragmentのテストを書いていました。
弊社では、Fragmentのテストは(今時点では)書かないかも知れませんが、それ以外は書いた方が良さそうだなと思います。UseCaseの使い所って、どんな”ユースケース”だろう?
シンプルなロジックの場合、UseCaseを使うとオーバースペックになる可能性があると感じます。
(ViewModelにRepositoryをインジェクトすれば済むケースもありそう)UseCaseの使い所としては、例えば、以下の様な状況があり得そうだと感じました。
- Repositoryの複数メソッドを組み合わせて結果を得たい
- その他のDomain Service (or Application Service)を組み合わせて結果を得たい
いかがでしたでしょうか
UseCaseが活用できるようになるには、ドメインを分析し、その結果をDomain Model / Domain Service クラスとして定義できている事が前提の様に感じました。
導入するには、周囲の人を巻き込んでいかないといけないですね。弊社モバイルチームでは、週に1回DDDの勉強会を行なっていますので、今回の知見を元に更にブラッシュアップをしていこうと思います。
参照
- Android Architecture Blueprints - Use Cases/Interactors in Domain layer