- 投稿日:2022-02-28T23:06:42+09:00
Jetpack ComposeでMaterial
はじめに こんにちは。Anroidエンジニア歴約二年のgottieです。 業務ではAndroid Classic Viewを触れていますが、個人ではせっせとJetpack Composeで遊んでいます。 今回はJetpack ComposeでのMaterial Colorの決め方を紹介します。 Android Classic Viewでもそうですが、いつも適当に色を決めていって、Darkモードなどを考慮すると収集がつかなくなっていたいたのですがJetPack ComposeはMaterial Colorとの親和性が高く、簡単にプロダクトに取り込むことができます。 方法 1.色を定義 アプリで使用するメインの色(primary)として一色決めます。色の濃さは中間が推奨されているで500とします。(こちら) メインのサブ(primaryVaiant)も必要ですが、ライトテーマとダークテーマに備えてメインの色より濃いものと薄いものを準備します。 アプリで使用するサブの色(secondary)として一色決めます。こちらもメインの色と同じで濃さが中間のものを一つ選び、そのサブとして濃いものと薄いものを準備します。 Gray系の色は何種類も使用してよいので、白から黒まで一通り準備します。 エラーは赤色(0xBFF00020)を使用しましょう。アプリのテーマとして赤を使用する場合はエラー色の検討が必要です。 Color.kt // PRIMARY val LightBlue300 = Color(0xFF4FC3F7) val LightBlue500 = Color(0xFF03A9F4) val LightBlue700 = Color(0xFF0288D1) // Secondary val LightGreen200 = Color(0xFFC5E1A5) val LightGreen500 = Color(0xFF8BC34A) val LightGreen800 = Color(0xFF558B2F) val White = Color(0xFFFFFFFF) val Gray100= Color(0xFFCFD8DC) val Gray300 = Color(0xFFE0E0E0) val Gray500 = Color(0xFF9E9E9E) val Gray700 = Color(0xFF616161) val Gray900 = Color(0xFF212121) val Black = Color(0xFF000000) val Error = Color(0xBFF00020) 2.ライトテーマとダークテーマで使用する色を定義する。 Color.ktで定義した色をライトテーマ用、ダークテーマ用として定義します。 ダークテーマにはより薄い色を、ライトテーマにはより濃い色を適用するとよさげです。(個人の見解) backgroundとsurfaceのカラーは微妙に変えておくことで多重ダイアログなどのときに背景が重なることを避けらると思います。 Theme.kt private val DarkColorPalette = darkColors( primary = LightBlue500, primaryVariant = LightBlue300, secondary = LightGreen500, secondaryVariant = LightGreen200, background = Gray900, surface = Gray700, error = Error, onPrimary = White, onSecondary = White, onBackground = White, onSurface = White, onError = White ) private val LightColorPalette = lightColors( primary = LightBlue500, primaryVariant = LightBlue700, secondary = LightGreen500, secondaryVariant = LightGreen800, background = Gray100, surface = Gray300, error = Error, onPrimary = Black, onSecondary = Black, onBackground = Black, onSurface = Black, onError = Black ) 終わり ダークテーマにしたら文字が見えない!!! ダイアログが背景色と被る!!! などの不具合から解放されました。 Jetpack Compose楽しい~ 参考 Material Design The color system 公式ドキュメント
- 投稿日:2022-02-28T19:21:24+09:00
JacocoMerge が Deprecated になったので、JacocoReport でレポートをマージする
概要 カバレッジ計測ツール JaCoCo の Gradle プラグインで、JacocoMerge が Deprecated になりましたので、 これを使わない方法でマルチモジュールの JaCoCo レポートをマージするように変更しました。 背景 Gradle6系 + Jacoco + マルチモジュール + フルKotlin + Android + Robolectric環境でユニットテストのカバレッジを出す の記事を参考にして、マルチモジュールの Android アプリプロジェクトで JaCoCo のレポートをマージしていました。 Deprecated. JacocoMerge より The JacocoReport task accepts multiple execution files as an input. This task type provides duplicated functionality and will be removed in Gradle 8.0. JaCoCo Gradle プラグインの JacocoMerge は残念ながら Deprecated となってしまいました。 Gradle 8.0 で削除予定ということで、いつまでも使い続けるわけにはいきません。 修正 先ほどの引用の通り、 JacocoReport が複数の実行ファイルを扱えるようになったので、それを利用します。 変更前 私のプロジェクトの場合は、ルートの build.gradle に以下の記述をして、 $ gradlew jacocoTestReport で各モジュールのカバレッジレポートを作成してから、 $ gradlew jacocoMergedTestReport を実行することで、マージされたレポートを作成していました。 build.gradle task mergeJacocoFiles( type: JacocoMerge, group: "verification" ) { gradle.afterProject { project, _ -> if (project.rootProject != project && project.plugins.hasPlugin('jacoco')) { executionData "${project.buildDir}/jacoco/testDebugUnitTest.exec" } } } task jacocoMergedTestReport( type: JacocoReport, dependsOn: [tasks.mergeJacocoFiles], group: "verification" ) { getExecutionData().from = mergeJacocoFiles.destinationFile gradle.afterProject { project, _ -> if (project.rootProject != project && project.plugins.hasPlugin('jacoco')) { getSourceDirectories().from += "${project.projectDir}/src/main/java" getClassDirectories().from += project.fileTree(dir: "${project.buildDir}/tmp/kotlin-classes/debug") } } reports { xml.enabled = true html.enabled = true } } 変更全体 差分は以下の通りです。 -task mergeJacocoFiles( // 元記事では jacocoMerge タスク - type: JacocoMerge, - group: "verification" -) { - gradle.afterProject { project, _ -> - if (project.rootProject != project && project.plugins.hasPlugin('jacoco')) { - executionData "${project.buildDir}/jacoco/testDebugUnitTest.exec" - } - } -}// ここまで 1 - task jacocoMergedTestReport( type: JacocoReport, - dependsOn: [tasks.mergeJacocoFiles], // 2 group: "verification" ) { - getExecutionData().from = mergeJacocoFiles.destinationFile // 3 - gradle.afterProject { project, _ -> if (project.rootProject != project && project.plugins.hasPlugin('jacoco')) { + getExecutionData().from += "${project.buildDir}/jacoco/testDebugUnitTest.exec" // 4 具体的には以下の変更をしました。 出力先を変更していた mergeJacocoFiles タスクを削除 jacocoMergedTestReport からその依存(dependsOn の行)を削除 jacocoMergedTestReport の1行目に書いていた getExecutionData().from = mergeJacocoFiles.destinationFile を削除 ループの中に executionData を移動させるコードを追加 実行 修正前と同様に、 $ gradlew jacocoTestReport で各モジュールのカバレッジレポートを作成してから、 $ gradlew jacocoMergedTestReport を実行することで、マージされたレポートが作成されました。 おわりに JaCoCo Gradle プラグインの JacocoMerge が Deprecated になりました。 jacocoReport を使ってマージするようにタスクを書き換え、複数モジュールの JaCoCo レポートをマージするように変更しました。
- 投稿日:2022-02-28T18:45:52+09:00
[Android]Paging3でDBから取得したリストの途中に別のデータを追加する
Android の Paging ライブラリで、データベースから取得したリストの途中に別のデータを追加する方法について考えたので紹介します。 例としては、例えば以下のように TODO タスクの一覧の中に広告を表示するというような感じです。 この TODO タスクはデータベースに保存されているものを参照しています。 考えた方法としては以下の2通りになります。 PagingData の insertSeparators メソッドを使う カスタムの PagingSource を作成する この記事を書いた時点では Paging 3.1.0 で試しています。 サンプルプロジェクトを こちら にアップしているので、よかったら参考にしてください。 1. PagingData の insertSeparators を使う方法 これは 公式のドキュメント にも記載されている方法です。 データベースから取得したデータからどの位置に追加データを挿入するかがわかる場合に使える方法です。 まずは Dao で以下のように定義します。 @Dao interface TodoDao { @Query("SELECT * FROM todos") fun getTodos(): PagingSource<Int, TodoEntity> } どの位置に追加データを挿入するかを、データからではなく先頭から何番目かで判定したい場合、以下のように SQL を書き換えれば行番号がデータに含まれるようになります。 data class TodoWithNumber( val id: Int, val title: String, val completed: Boolean, val number: Int ) @Dao interface TodoDao { @Query( """ SELECT T1.*, COUNT(*) AS number FROM todos AS T1 INNER JOIN todos AS T2 ON T1.id >= T2.id GROUP BY T1.id, T1.title, T1.completed """ ) fun getTodos(): PagingSource<Int, TodoWithNumber> } SQLite では ROW_NUMBER() という行番号が取得できるウィンドウ関数がありますが、この関数は SQLite 3.25 から使えます。 ドキュメントに記載されている OS バージョンごとの SQLite のバージョンを見ると API レベル30以降からしか使えないので、このウィンドウ関数を使うのはまだ難しそうです。。 次に UiModel を作成します。リストに表示する際に使用するデータになります。これは sealed interface(もしくは sealed class) にしておくと RecyclerView で表示する時に扱いやすくなります。 sealed interface UiModel { data class Todo(val id: Int, val title: String, val completed: Boolean) : UiModel data class Ad(val id: Int, val title: String) : UiModel } 以下は ViewModel 内で PagingData の Flow を作成する例です。 PagingData の insertSeparators メソッドを使って、TODO タスク4つおきに広告を1つ表示するようにデータを挿入しています。 private val pagingConfig = PagingConfig(...) val todosFlow: Flow<PagingData<Model>> = Pager(config = pagingConfig) { todoDao.getTodos() } .flow .map { pagingData -> pagingData .insertSeparators { before: TodoWithNumber?, _ -> val rowNumber = before?.number ?: return@insertSeparators null // TODOタスク4つおきに広告を1つ表示する. if (rowNumber % 4 == 0) UiModel.Ad(id = rowNumber, title = "広告-$rowNumber") else null } .map { // UiModel に変換する. when (it) { is TodoWithNumber -> UiModel.Todo(id = it.id, title = it.title, completed = it.completed) is UiModel.Ad -> it else -> throw IllegalArgumentException() } } } .cachedIn(viewModelScope) 以下は RecyclerView で使用する Adapter の例です。 PagingDataAdapter を継承する必要があります。 先ほど作成した UiModel の種別によって使用する ViewHolder が変わります。 class TodosAdapter : PagingDataAdapter<UiModel, RecyclerView.ViewHolder>(TodoComparator) { override fun getItemViewType(position: Int): Int { // getItem() ではなく peek() を使うことによって、ページの取得・破棄のトリガーを避けることができる. return when (peek(position)) { is UiModel.Todo -> R.layout.list_item_todo is UiModel.Ad -> R.layout.list_item_ad null -> throw IllegalStateException("Unknown view") } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { R.layout.list_item_todo -> TodoViewHolder(parent) else -> AdViewHolder(parent) } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val item = getItem(position) ?: return when (holder) { is TodoViewHolder -> holder.bind(item as UiModel.Todo) is AdViewHolder -> holder.bind(item as UiModel.Ad) } } } private object TodoComparator : DiffUtil.ItemCallback<UiModel>() { override fun areItemsTheSame(oldItem: UiModel, newItem: UiModel): Boolean { val isSameTodo = oldItem is UiModel.Todo && newItem is UiModel.Todo && oldItem.id == newItem.id val isSameAd = oldItem is UiModel.Ad && newItem is UiModel.Ad && oldItem.id == newItem.id return isSameTodo || isSameAd } override fun areContentsTheSame(oldItem: UiModel, newItem: UiModel): Boolean { return oldItem == newItem } } 2. カスタムの PagingSource を作成する方法 続いてカスタムの PagingSource を作成する方法です。こちらの方法だと insertSeparators を使用する方法に比べてより複雑なことが可能になるかと思います。 実装にあたっては LimitOffsetPagingSource を参考にしています。 Dao の戻り値に PagingSource を設定した時、実体はこのクラスのインスタンスになっています。 まずは Dao で以下のように offset と limit が指定できるメソッドと、TODO タスクの総数を取得するメソッドを用意します。 @Dao interface TodoDao { @Query("SELECT * FROM todos LIMIT :limit OFFSET :offset") suspend fun getTodos(offset: Int, limit: Int): List<TodoEntity> @Query("SELECT COUNT(*) FROM todos") suspend fun countTodos(): Int } 次にカスタムの PagingSource です。 以下のように PagingSource を継承したクラスを作成します。(一部抜粋です) こちらの例では TODO タスク5つおきに広告を1つ挿入しています。 private const val AD_POSITION_INTERVAL = 5 class TodoPagingSource( private val db: AppDatabase ) : PagingSource<Int, UiModel>() { private val todoDao: TodoDao = db.todoDao() private val itemCount: AtomicInteger = AtomicInteger(-1) private val adPositions = AtomicReference<List<Int>>(emptyList()) override fun getRefreshKey(state: PagingState<Int, UiModel>): Int? { val initialLoadSize = state.config.initialLoadSize return state.anchorPosition?.let { anchorPosition -> maxOf(0, anchorPosition - (initialLoadSize / 2)) } } override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UiModel> { val tempCount = itemCount.get() return if (tempCount < 0) { initialLoad(params) } else { loadFromDb(params, tempCount) } } private suspend fun initialLoad(params: LoadParams<Int>): LoadResult<Int, UiModel> { return db.withTransaction { val tempCount = todoDao.countTodos() itemCount.set(tempCount) // 広告の位置を5つおきになるようにリストを設定. adPositions.set((AD_POSITION_INTERVAL..tempCount step AD_POSITION_INTERVAL).toList()) loadFromDb(params, tempCount) } } private fun getAdPositions(startIndex: Int, endIndex: Int): List<Int> { val positions = adPositions.get() val range = startIndex until endIndex return positions.filter { it in range } } private suspend fun loadFromDb( params: LoadParams<Int>, itemCount: Int, ): LoadResult<Int, UiModel> { val key = params.key ?: 0 val limit: Int = getLimit(params, key) val offset: Int = getOffset(params, key, itemCount) val todos = todoDao.getTodos(offset, limit) val nextPosToLoad = offset + todos.size val nextKey = if (todos.isEmpty() || todos.size < limit || nextPosToLoad >= itemCount) { null } else { nextPosToLoad } val prevKey = if (offset <= 0 || todos.isEmpty()) null else offset val todoModels = todos.map { UiModel.Todo(it.id, it.title, it.completed) } val adPositions = getAdPositions(offset, (offset + limit)) val uiModels: List<UiModel> = if (adPositions.isNotEmpty()) { buildList { var start = 0 // 広告の位置のリストを元に UiModel のリストを作成. adPositions.forEach { adPosition -> val end = adPosition - offset if (start < end) { addAll(todoModels.subList(start, minOf(end, todoModels.size))) } add(UiModel.Ad(id = adPosition, title = "広告-$adPosition")) start = end } if (start < todoModels.size) { addAll(todoModels.subList(start, todoModels.size)) } } } else { todoModels } val itemsBefore = offset + getAdPositions(0, offset - 1).size val afterAdCount = getAdPositions(nextPosToLoad, itemCount).size val itemsAfter = maxOf(0, itemCount + afterAdCount - nextPosToLoad) return LoadResult.Page( data = uiModels, prevKey = prevKey, nextKey = nextKey, itemsBefore = itemsBefore, itemsAfter = itemsAfter ) } private fun getLimit(params: LoadParams<Int>, key: Int): Int { return when (params) { is LoadParams.Prepend -> if (key < params.loadSize) key else params.loadSize else -> params.loadSize } } private fun getOffset(params: LoadParams<Int>, key: Int, itemCount: Int): Int { return when (params) { is LoadParams.Prepend -> if (key < params.loadSize) 0 else (key - params.loadSize) is LoadParams.Append -> key is LoadParams.Refresh -> if (key >= itemCount) maxOf(0, itemCount - params.loadSize) else key } } } PagingSource を継承したクラスでは load() と getRefreshKey() メソッドの2つをオーバーライドする必要があります。 load() メソッドにおいて、Dao を使ってデータベースからデータを取得し、そのデータに広告を挿入しています。 上記以外にもデータベースの対象テーブルに変更があった場合の処理などを実装する必要があるので、全ての実装は サンプルプロジェクト を確認してみてください。 そして先ほどの例と同じように ViewModel 内で PagingData の Flow を作成します。 val todosFlow: Flow<PagingData<UiModel>> = Pager(config = pagingConfig) { TodoPagingSource(db) } .flow .cachedIn(viewModelScope) Adapter などは先ほどの例と同じものが使用できます。 どちらの方法を使うべきか? まずは insertSeparators を使う方法を検討してみるのがよいかと思います。 こちらで実現が可能であれば、おそらくこちらの方が実装がシンプルになるはずです。 insertSeparators の方法では実現できなければ、カスタムの PagingSource を作成する方法を検討してみましょう。 参考 Transform data streams LimitOffsetPagingSource.kt
- 投稿日:2022-02-28T18:03:54+09:00
AndroidのFirebase Dynamic Linksをお試し
Android12からhttpsでのディープリンクができなくなったという記事を見たこともあり、今後はhttpsでのアプリ起動する場合、FirebaseのDyanamic LinksもしくはAppLinksが必要そうだったので調査しました。 その記事はこちら まずはFirebaseのDynamic Linksについて調べてみたのでまとめです。 1.Firebase Dynamic Linksとは? Android,iOSの両方の端末でURLをクリックした際に、アプリを起動できます。その際にアプリがインストールされていない場合、GooglePlayStore、AppStoreの該当ページに遷移したり設定したページへの遷移なんかができたりします。 URLSchemeと違って、起動時に複数のアプリが存在する場合にはアプリ選択画面が出てくることなく、該当のアプリを直接起動できます。 2.Firebaseプロジェクトの設定 Dynamic Linksを利用するにはまず、Firebaseのプロジェクト作成が必要になります。 該当のアプリを選択して、プロジェクトの設定を行います。 プロジェクトの全般 -> アプリ追加からAndroidのアプリを追加 手順に従って、jsonファイルを該当のプロジェクトに配置します。 SHA-1の登録がありますが一旦スキップしてしまってOKです。 プロジェクトにgoogle-services.jsonを設置 アプリで署名して使うkeystoreのSha1とSha256の情報をプロジェクトに追加します。 下記のコマンドで取得できます。 keytool -list -v -keystore [keystoreのパス] 取得したSha1とSha256を登録します。(画像の赤枠部分です) Firebaseの設定は完了です。あとはDynamic Links側の設定をします。 3.Dynamic Linksの設定 独自のURLを発行し、アプリがなかった時などの設定を行います。 FirebaseのメニューのDynamic Linksを選択します。 ドメインを作成します。こちらは利用されていないものにする必要があるので、xxxx.page.linkのxxxxの部分を独自の文字列にすればOKです。 ドメインに問題がなければ下記のように作成されます。 そのあと、「新しいダイナミック リンク」からURLを作成します。 押下すると下記の画面が表示されるので、各々設定していきます。 ダイナミック リンクの設定です。今回はテストで入れているのですが、実際はアプリの紹介ページなどになります。 アプリがインストールされていない場合(設定による)やPCで該当のURLを開いた場合にはこちらが開くようになります。 Android用のリンク動作を定義できて、アプリ内でディープリンクをハンドリングするかやインストールされていなかった場合にGooglePlayのページを開くなどの設定が可能です。 「dynamictest」となっていますが、こちらは今回実際にアプリを起動させる対象になるので最初の流れで作成したFirebaseプロジェクトの値を設定しておきます。 ここまで設定できたら、Dynamic Links側の設定も完了です。 4.アプリ側の設定 アプリ側の設定についてですが、AndroidStudioを使っていると比較的簡単に設定してくれます。 AndroidStudioのTools -> App Links Assistantを選択 メニューが表示されるので、「Open URL Mapping Editor」を押下します。 Hostに先ほど作成した、URLを入れます。Activityには起動したいActivityを入れておきます。 こちらでOKを押すと、AndroidManifest.xmlに追加されます。 Webサイトに設置されているものと一致しているかの確認ができるのですが、こちらdebugだとうまくいきますが選択したkeystoreだとうまくいきませんでした。。。 そのため、別途下記のサイトでチェックしました。 https://developers.google.com/digital-asset-links/tools/generator 各項目を入れて「Test statement」を実施して、Successになれば大丈夫です。 下記の「Generate Digital Asset Links file」とありますが、今回はFirebaseのDynamic Links側で自動で生成してくれるので不要なため、そのまま閉じてしまってOKです。 これで全て準備が終わったので、端末にアプリを入れて起動してみます。 ブラウザでURLを入力すると、一瞬プログレスダイアログが表示された後でアプリが起動します。 アプリ側でハンドリングしたい場合 アプリの起動までは出来たのですが、案件によってはURLによって起動する画面を選びたいみたいなことが発生すると思います。 その場合にはアプリ側で処理をハンドリングできるようにアプリ側に設定が必要です。 公式ページが分かりやすく記載あるので、下記を参考にするといいと思います。 公式ページ まずはFirebaseの設定を有効にするために、アプリのprojectのgradleにpathを追加 アプリのモジュール側のgradleのpluginsに追加 同様にアプリのモジュール側のgradleにDynamic Linksで利用するライブラリ追加 firebase-bomというのを使うとFirebase関連のライブラリの関係を解決してくれるようなので、一つ一つ設定する必要がなくて良きです。 利用しない場合は、bomを外して各々のfirebase-dynamic-linksとfirebase-analyticsにバージョン指定すればOKです。 起動する先に指定したActivityでDynamic Linksのデータを受け取れるように記述します。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) getDynamicLinks(intent) } private fun getDynamicLinks(intent: android.content.Intent) { FirebaseDynamicLinks.getInstance() .getDynamicLink(intent) .addOnSuccessListener(this) { pendingDynamicData -> if (pendingDynamicData != null) { Toast.makeText( applicationContext, " dynamic links url ${pendingDynamicData.link}", Toast.LENGTH_LONG ).show() } } } } これでURLが取得できるようになりました。テストでToastを表示するようにしてます。 Dynamic Linksの設定されているディープリンクのURLが取得できます。 こちらのURLにクエリをつけるなどすればそちらをパースすることでアプリ側で出し分けなどは可能です。 5.まとめ Firebase Dynamic Linksについて調べてみました。導入から実行まで比較的簡単に試せるのが分かっていただけたのでは?と思います。 AppLinksと比べるとホストの設定等の手間がなく、気楽にURLでの起動が試せるのが良かったです。 「インストールされていなかったら、ストアに飛ばしたい」や「あるWebページに飛ばしたい」となるとWebページ側で対応が必要だったりしますし、セキュアな情報をやりとりしようとした際にはURLSchemeだと別のアプリでも起動できる可能性があったりと安全ではない部分があります。 Firebase Dynamic Linksを利用すれば、そういったところが簡単に実装できるので広告で利用したり、LPページにダウンロードリンクを付けたりするときに便利ですし、外部の決済サービスとの連携を行ったりする場合にも利用できるのではないかと思います。
- 投稿日:2022-02-28T16:48:15+09:00
リフレクションでKotlinの要素を取得する【Kotlinリフレクション編】
Kotlinリフレクションを使用したKotlin要素の取得方法についてまとめました。 リフレクションは、privateなクラスやメソッドにアクセスするために便利な機能です。 前回のJavaリフレクション編に引き続き、今回もAndroidの単体テスト例を用いてまとめています。 尚、ご意見や筆者の認識の誤り等がありましたら、お気軽にコメントをお願いいたします。 まとめ JavaリフレクションとKotlinリフレクション 今回、Kotlinリフレクションでは以下の要素が取得できると分かりました。 public クラス public / private プロパティ public / private メソッド public / private コンストラクタ そして、privateクラスのみ取得ができませんでした。 そのため、基本的にはKotlinリフレクションを使用し、privateクラスを取得する時のみJavaリフレクションを使用すると良いのではと思います。 取得する要素と使用するメソッドのまとめ public クラス ::classで取得 public / private プロパティ publicプロパティの取得はクラス名::プロパティ名 privateプロパティの取得はKClass型.memberProperties、isAccessible = true指定 public / private メソッド publicメソッドの取得はクラス名::メソッド名 privateメソッドの取得はKClass型.declaredMemberFunctions、isAccessible = true指定 public / private コンストラクタ kClass.constructorsで取得、privateコンストラクタではisAccessible = true指定 使用するための準備 【確認環境】 Kotlin version:1.5.20 kotlin:kotlin-reflect:1.5.20 Kotlinリフレクションを使うためには、パッケージの追加が必要です。 AndroidStudioで新規プロジェクトを作成する場合はデフォルトで追加されていますが、万が一リフレクションが使えない!となった場合は、以下の依存関係を確認をしましょう。 build.gradle(app) dependencies { implementation 'org.jetbrains.kotlin:kotlin-reflect:1.5.20' } Kotlinリフレクション使用例 今回も以下のソースコードをもとに、Kotlinリフレクションを使用して要素を取得、テストを実施します。 尚、今回使用する以下のコードはリフレクションの事例をまとめるために使用するもので、不完全な部分を含みますのでご了承ください。 //Curryクラス (publicコンストラクタを持つクラス) class Curry(potato: Int) { val publicP = "publicPotato: ${potato}kg" private val privateP = "privatePotato: ${potato}kg" var ppp = "ppp" fun publicM(carrot: Int): String{ return "publicCarrot: ${carrot}kg" } private fun privateM(carrot: Int): String{ return "privateCarrot: ${carrot}kg" } private class MakeCurry(onion: String, val meat: Int){ val nestPublicP = "nestPublic-$onion" private val nestPrivateP = "nestPrivate-$onion" fun nestPublicM(water: Int): String { return "nestPublicMeat: ${meat+water}kg" } private fun nestPrivateM(water: Int): String { return "nestPrivateMeat: ${meat+water}kg" } } } //Stewクラス (privateコンストラクタを持つクラス) class Stew private constructor(val potato: Int, val carrot: Int) { val publicP = "publicPotato: ${potato}kg" private fun privateM(onion: Int): String { return "${publicP}, privateNetWeight: ${potato+carrot+onion}kg" } } publicクラスの要素 publicプロパティの取得 publicプロパティは、 ::オペレーターで取得可能です。 ::オペレーターは、内部にてKProperty 型のプロパティオブジェクトとして処理されます。 このKPropertyはkotlin.reflectionに含まれるクラスで、その名の通りプロパティを表します。 余談ではありますが、kotlin.reflectionには他にも、KClassクラス、KFunctionクラス、KCallableクラスなどがあります。 今回publicプロパティを取得するために使用するメソッドは、以下の2つです。 クラス名::プロパティ名 KClassを取得します。 KProperty.get(インスタンス) 指定したインスタンス時のプロパティの値を取得するためのメソッドです。 @Test fun getPublicP() { // テストのため通常通りプロパティを取得する(リフレクションではない) val actual = Curry(2).publicP //①KProperty1を取得 val kProperty = Curry::publicP //②プロパティの値を取得 val expected = kProperty.get(Curry(2)) //検証 assertEquals(expected, actual) } また、Javaリフレクション使用時は定数(val)でも値をsetできましたが、kotlinリフレクションではsetが使えるのは変数(var)の場合のみのようです。 変数のset時には、以下のメソッドを使用します。 KMutableProperty.set(インスタンス, setしたい値) setメソッドは、変数として宣言されたプロパティを表すためのKMutablePropertyクラスに実装されています。 また、::プロパティ(変数)名を指定すると自動でKMutablePropertyが取得されます。 //①変数を取得 val kMutableProperty1 = Curry::ppp //②setを実装 kMutableProperty1.set(Curry(2), "aaa") privateプロパティの取得 privateプロパティの場合、::オペレーターを使用して直接KPropertyを取得することはできません。 そのため、直接KPropertyを取得するのではなく、KClassを経由してプロパティを取得します。 privateプロパティを取得するために使用するリフレクション関係メソッドは以下の5つです。 クラス名::class KClassを取得します。 KClass.memberProperties 当該クラスとそのスーパークラスで宣言されている非拡張プロパティを取得するメソッドです。 戻り値はCollection<KProperty1<KClassのクラス名, *>>型です。 KProperty.name プロパティの名前を返すメソッドです。 今回はこれを用いて、プロパティの判別を行います。 isAccessible 取得した要素へのアクセスを許可するためのメソッドです。 = trueを設定することで、privateな要素へのアクセスが可能になります。 このisAccessibleメソッドは、KPropertyとKFunctionのスーパータイプにあたるKCallableに実装されています。 KProperty型.get(インスタンス) 指定したインスタンスの時の、プロパティの値を取得するメソッドです。 @Test fun getPrivateP() { //①KClassを取得 val kClass = Curry::class //②プロパティ配列を取得(今回はprivatePとpublicPを持つ配列が取得される) val kPropertyC = kClass.memberProperties //③配列内の要素をforEach文で取り出し、名前で条件分岐する kPropertyC.forEach { //it:KProperty1<Curry, *> if(it.name == "privateP") { //④アクセスを許可 it.isAccessible = true //⑤プロパティの値を取得 val actual = it.get(Curry(2)) //検証 assertEquals("privatePotato: 2kg", actual) } } } publicメソッドの取得 publicメソッドを取得するために今回使用するメソッドは以下の2つです。 ::メソッド名 KFunctionを取得します。 call(インスタンス, メソッドの引数1, 引数2, …) 指定された引数のリストを使用して、結果を返すメソッドです。 以下の例ではkFunctionに作用し、メソッドが実行された結果を返します。 このcallメソッドは、KPropertyとKFunctionのスーパータイプにあたるKCallableに実装されています。 @Test fun publicMTest() { //テストのため通常通りメソッドを実行(リフレクションではない) val actual = Curry(2).publicM(1) //①KFunctionを取得 val kFunction = Curry::publicM //②メソッドを実行 val expected = kFunction.call(Curry(2), 1) //検証 assertEquals(expected, actual) } privateメソッドの取得 privateメソッドの場合、::オペレーターを使用して直接KFunctionを取得することはできません。そのため、KClassを経由してプロパティを取得します。 今回privateメソッドを取得するために使用するメソッドは以下の4つです。 クラス名::class KClassを取得します。 KClass.declaredMemberFunctions クラス内に設置した関数を取得するためのメソッドです。 戻り値は、 Collection>型です。 isAccessible 取得した要素へのアクセスを許可するためのメソッドです。 = trueを設定することで、privateな要素へのアクセスが可能になります。 このisAccessibleメソッドは、KPropertyとKFunctionのスーパータイプにあたるKCallableに実装されています。 call(インスタンス, メソッドの引数1, 引数2, …) 指定された引数のリストを使用して、結果を返すメソッドです。 以下の例ではitであるKFunctionに作用し、メソッドが実行された結果を返します。 このcallメソッドも、KCallableに実装されています。 @Test fun privateMTest() { //①KClassを取得 val kClass = Curry::class //②関数配列を取得 val kFunction = kClass.declaredMemberFunctions //③配列内の要素をforEach文で取り出し、名前で条件分岐する kFunction.forEach { //it: KFunction<*> if (it.name == "privateM"){ //④アクセスを許可 it.isAccessible = true //⑤メソッドを実行し、戻り値を取得 val actual = it.call(Curry(2), 3) //検証 assertEquals("privateCarrot: 3kg", actual) } } } privateクラスの要素 今回確認した範囲では、privateクラスは取得できませんでした。 そのため、privateクラスの要素を取得する場合にはJavaリフレクションを使用することになると考えています。 Javaリフレクションを使用したprivateクラスの取得方法 privateコンストラクタを持つpublicクラスの要素 publicプロパティの取得 privateコンストラクタを持つクラスのpublicプロパティを取得するために、以下の5つのメソッドを使用します。 クラス名::class KClassを取得します。 KClass.constructors 当該クラスで宣言された全てのコンストラクタを取得するためのメソッドです。 戻り値は、Collection>です。 isAccessible 取得した要素へのアクセスを許可するためのメソッドです。 = trueを設定することで、privateな要素へのアクセスが可能になります。 このisAccessibleメソッドは、KPropertyとKFunctionのスーパータイプにあたるKCallableに実装されています。 call(インスタンス, メソッドの引数1, 引数2, …) 指定された引数のリストを使用して、結果を返すメソッドです。 以下の例ではitであるKFunctionに作用し、インスタンスが生成されます。 このcallメソッドも、KCallableに実装されています。 インスタンス::プロパティ名.get() プロパティの値を取得するメソッドです。 @Test fun getPublicP() { //①KClassを取得 val kClass = Stew::class //②コンストラクタ配列を取得 val const = kClass.constructors //③配列内の要素をforEach文で取り出す const.forEach { //it: KFunction<Stew> //④アクセスを許可する it.isAccessible = true //⑤インスタンスを生成し、プロパティの値を取得する val actual = it.call(1, 2)::publicP.get() //検証 assertEquals("publicPotato: 1kg", actual) } } privateメソッドの取得 流れとしては、 privateコンストラクタを持つクラスのpublicプロパティの取得の前半(配列内の要素をforEach文で取り出すまで)と publicクラスのprivateメソッドの取得の後半(関数配列の取得以降) を組み合わせて取得できます。 ただし、今回の例ではコンストラクタは1つしか実装していないため、②にてコンストラクタ配列を取得後は1つ目の要素のみ取得しています。 @Test fun privateM() { //①KClassを取得 val kClass = Stew::class //②コンストラクタ配列の1つ目を取得(constはKFunction<Stew>) val const = kClass.constructors.first() //③コンストラクタへのアクセスを許可 const.isAccessible = true //⑤インスタンスを生成する val kFunction = const.call(1, 2) //⑥関数配列を取得し、配列内の要素をforEach文で取り出す kFunction::class.declaredMemberFunctions.forEach { it -> //it: KFunction<*> //⑦名前で条件分岐する if (it.name == "privateM") { //⑧メソッドへのアクセスを許可する it.isAccessible = true //⑨メソッドを実行する val actual = it.call(kFunction, 4) //検証 assertEquals("publicPotato: 1kg, privateNetWeight: 7kg", actual) } } } 参考 Reflection | Kotlin Qiita:Kotlin リフレクション hatenablog:Kotlinのdata classのpropertyをreflectionで更新する - abcdefg..... hatenablog:Kotlinのリフレクション(protected/privateメソッド呼び出し)
- 投稿日:2022-02-28T13:49:23+09:00
スマホでのタップ時に四角い枠を表示させないようTailwind CSSで設定する方法
僕が子ども向けのブラウザゲームを作成する中でのことです。 操作感を向上させるため、ボタンがタップに反応する領域を、ボタンの視覚的な領域よりも広くしました。 その結果悩まされたのが、タップ時に表示される、四角い影です。 CSSであれば、以下のように書けば、この影が消えます(一般化のため、aタグにしてあります)。 a { -webkit-tap-highlight-color:rgba(0,0,0,0); } しかし、Tailwindではこれに対応するクラスが存在しません。 対応策としては、tailwind.config.jsに、以下のように記述してください。 const plugin = require('tailwindcss/plugin') module.exports = { // ... plugins: [ plugin(function ({ addUtilities }) { addUtilities({ '.no-tap-highlighting': { '-webkit-tap-highlight-color': 'rgba(0,0,0,0)', } }) }) ] } これで、no-tap-highlightingというクラスを使用すれば、タップ時に四角い影が表示されなくなります。 参考 https://tailwindcss.com/docs/adding-custom-styles https://serversideup.net/using-tailwindcss-to-design-your-mobile-app/