- 投稿日:2021-12-03T21:07:53+09:00
アイコン作成等のツールリンク集
たまたまどこかで知り、便利じゃん!と思ってブックマークに入れたは良いが、 本当に必要なときに結局どこにしまったか忘れているので、 個人的にたまに使ったりするツール系のリンクを備忘録としてまとめてみました。 Android/iOS AndroidAssetStudio(各種アイコン作成) MakeAppIcon(アイコン作成) resizeappicon(アイコン作成) (Android用)BundleToolを使用してaabファイルを実機にインストール 画像系 RemoveBackground(透過画像作成) MethodDraw(SVGエディタ) SvgPathEditor(SVGエディタ) フォント系 Google Fonts MaterialDesignIcons その他 Regexper(正規表現の可視化)
- 投稿日:2021-12-03T18:17:29+09:00
Androidの多機能Fragmentを再利用可能にした話
はじめに この記事は、eureka Advent Calendarの17日目の記事です。 本日は今年の4月にeurekaに新卒のAndroidエンジニアとして入社した私が担当させていただきます。 この記事では、複数の画面で共通する多機能なFragmentを再利用可能な形に書き換えた際に自分にとって学びがあったことを紹介します。 概要 Pairsでは、2021年6月に新機能としてコミュニティチャットが実装されました。 これは、マッチング前のお相手とカジュアルにテキストベースでお話が出来る機能です。 これに加えて、コミュニティチャットにスタンプを投稿する機能が実装されることとなりました。 ところが、途中でマッチング中のお相手との1:1メッセージにもコミュニティチャットと同じスタンプを使えるようにすることとなりました。 そこでスタンプのFragmentを複数画面で利用可能な形にリファクタしたのですが、Fragmentの再生成や親Fragmentからの独立性を考慮した実装が必要だったのでその際に得た知見をまとめます。 前提:スタンプ関連のクラス構造 最終的に実装されたチャットやスタンプの画面構成は以下のようになりました。 同様に、状態管理を構造を図にすると以下のようになります。 コミュニティチャットや1:1メッセージのFragmentにスタンプFragmentが含まれる形となっていますが、スタンプの状態やサーバーからのスタンプ情報の取得はチャットと分離して管理することが出来ます。 これは共通したスタンプFragmentを複数の画面で利用するための第一歩であり、チャットとスタンプの状態管理を独立させることでそれぞれをシンプルに実装することが出来ます。 なお、スタンプを別モジュールに切り出すことが出来たのでコミュニティチャットのモジュールと1:1メッセージのモジュールがそれぞれスタンプモジュールを参照する構造となっています。 ところがチャットとスタンプは機能面で密接に関係しているため、状態管理を分離したことで連携方法を工夫させる必要が出てきました。 機能要件 スタンプFragmentの分離と共通化にあたって課題となった以下の3つの機能要件をピックアップして紹介します。 スタンプをタップしたらチャットに投稿する チャットに投稿されたスタンプをタップすると、そのスタンプが含まれるパッケージを表示する スタンプパッケージ1の選択やダイアログの表示・操作のログを送信する スタンプをタップしたらチャットに投稿する ※画面は開発中のものです。実際のユーザーのデータ等は使用されていません。 スタンプ一覧画面を開き、送信したいスタンプをタップするとチャットに投稿する機能です。 これを実装都合で見ていくと、以下のフローで実現されます。 スタンプFragmentがタップイベントを受け取る スタンプFragmentからチャットFragmentにスタンプがタップされた旨を伝える チャットFragmentからチャットにスタンプを投稿するAPIを叩く 今回問題となるのは、2番のスタンプFragmentからチャットFragmentにスタンプがタップされた旨を伝える部分です。 子Fragmentから親Fragmentへのイベント通知はどのように実装すればよいでしょうか? 採用した解決策 スタンプFragmentで実装したListener interfaceを親が実装し、スタンプFragmentのonAttachメソッドでローカル変数に代入する 具体的には以下のような実装になります。 StampFragment.kt interface OnStampClickListener { fun onStampClicked(stampId: Stamp.ID, stampPackage: StampPackage) } StampFragment.kt override fun onAttach(context: Context) { super.onAttach(context) val parentFragment = parentFragment if (parentFragment is OnStampClickListener) { onStampClickListener = parentFragment } val parentActivity = activity ?: return if (parentActivity is OnStampClickListener) { onStampClickListener = parentActivity } } こうすることで、Fragmentが再生成された場合でもonAttachでListenerを登録出来る実装になりました。 ボツ案 スタンプFragmentの生成時にコンストラクタでcallback関数を渡す この方法では、Fragmentの再生成に対応することが出来ません。 Fragmentの再生成時、AndroidはそのFragmentの無引数コンストラクタを呼び出してインスタンスを生成するためFragmentのコンストラクタは無引数でなければいけません。 理由については、stackOverFlowで詳しく書かれている回答がありますのでそちらを参照してください。 同じ理由で、コンストラクタ以外の関数でFragmentに渡そうとしても再生成に対応できなくなってしまいます。 チャットに投稿されたスタンプをタップすると、そのスタンプが含まれるパッケージを表示する ※画面は開発中のものです。実際のユーザーのデータ等は使用されていません。 チャットに投稿されたスタンプが気になったとき、それをタップするとそのスタンプが含まれるパッケージを表示することが出来る機能です。 これを実装都合で見ていくと、以下のフローで実現されます。 チャットFragmentがタップイベントを受け取る チャットFragmentからスタンプFragmentにスタンプがタップされた旨を伝える スタンプFragmentが対応するスタンプパッケージを表示する 今回問題となるのは、2番のチャットFragmentからスタンプFragmentにスタンプがタップされた旨を伝える部分です。 先ほどとは逆に、親Fragmentから子Fragmentへのイベント通知はどのように実装すればよいでしょうか? 採用された解決策 findFragmentByIdで見つけてくる これはFragmentManagerのメソッドで、指定したidを持つViewが表示しているFragmentを取得できます。公式解説はこちら。 こうすることで、Fragmentの再生成に対応しつつ子Fragmentのインスタンスを取得することが出来ます。 ボツ案 親であるチャットFragmentがスタンプFragmentのインスタンスを持つ 例えば以下のような実装です。 DummyParentFragment.kt var stampFragment: StampFragment? = null fun setupStampFragment() { stampFragment = StampFragment.newInstance( parentInfo = StampFragmentParentInfo.Talk(partnerId) ) supportFragmentManager.beginTransaction() .replace(binding.stickerFragmentContainer.id, stampFragment) .commitNow() } この状態でスタンプFragmentが再生成されてしまうと、親が変数で保持しているスタンプFragmentと実際に表示に利用されているスタンプFragmentが別のインスタンスになってしまいます。 そのため、チャットに投稿されたスタンプをタップしてもスタンプFragmentが適切に表示されないという問題が起こってしまいます。 スタンプパッケージの選択やダイアログの表示・操作のログを送信する Pairsではサービスの質の向上のため、ユーザーの操作やアプリの回遊のログを収集し分析しています。 スタンプについても、スタンプパッケージの選択やスタンプの購入動作等に対してログを収集しています。 ログにはいくつかのパラメータが含まれますが、今回問題になったのはコミュニティチャットと1:1メッセージのどちらから操作を行ったかです。 先述の通り、スタンプFragmentは親となるチャットFragmentがコミュニティチャットのものであるか1:1メッセージのものであるかを知りません。 この独立性を保ったまま、親Fragmentによって変化するログを送るにはどうすればよいでしょうか? 解決策 親Fragmentに関するログに必要なパラメータを持つdata classをsealed classでまとめる 具体的にはsealed classは以下のような実装になります。 StampFragmentParentInfo.kt sealed class StampFragmentParentInfo : Parcelable { @Parcelize data class CommunityChat( val communityId: Community.ID, val topicId: Topic.ID ) : StampFragmentParentInfo() @Parcelize data class Talk( val partnerId: String ) : StampFragmentParentInfo() } スタンプFragmentはこのsealed classのインスタンスを持ちますが、それがどちらのdata classなのかは知りません。 このインスタンスをログ送信用のhandlerに渡し、その先で実際に送信するログの形に変換します。 こうすることで、親Fragmentが何であるかを知らないまま親Fragmentに関するログを送信することが出来るようになりました。 この実装については事前に経験豊富な先輩からログの抽象化をしたほうがいいのではないか?と助言を頂いていたので、特にボツ案となる実装をせずに済みました。 おわりに 今回はAndroid開発において機能の多いFragmentを使い回す際の実装の工夫について紹介しました。 ここで紹介した機能は私一人で実装したわけではなく、設計段階からAndroidチーム内で何度も相談しながら進めていきました。 最近は定期的にAndroidチーム内でペアプロを実施しており、設計に関して議論したり新規参入メンバーに対するドメイン知識の共有をしたりする機会が増えてきています。 もし、そんなエウレカのAndroidチームや私の普段やっていることに興味をお持ちいただけたらぜひカジュアル面談しましょう! 今回のAdCに合わせてMeetyを作成してみましたので、こちらからご連絡ください。Twitterでのご連絡もお待ちしております。 スタンプのまとまりの単位です。スタンプ一覧の表示やスタンプの購入はパッケージ単位で行われます。 ↩
- 投稿日:2021-12-03T17:22:35+09:00
ZXing-android-embedded QRコードリーダーの、RegisterActivityResultとKotlin対応
スマホで簡易にQRコードリーダーを動かしたいとき、ZXingというのがあります。それを含むパッケージに、https://github.com/journeyapps/zxing-android-embedded というのがあります。この秋に、それがRegisterActivityResult対応し、Integratorとか機能をDeprecateしました。 それを取り込むのにひと手間あったので、ここへ記録。 1.zxing-android-embedded は、Javaコードしか提供していません。Kotlinほしい。Android StudioでそのSampleをコンバートすりゃいいのですが。ネットで調べた限り、まだ誰も載せていないので、以下にKotlinでのスケルトン載せておきます。これだけ。非常にSimple。 2.以前動いていたコードが、もう動かない、原因を調べたところ、ManifestでActivity属性で、android:noHistory="true" という変更をしたのが邪魔していた。なんでか、理解していない。誰か教えてくれ。 import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanOptions ... val barcodeLauncher = registerForActivityResult(ScanContract()) { result -> if (result.contents != null) { 読み取ったQRコードテキストの処理 } else { 失敗処理 } val options = ScanOptions() options.setOrientationLocked(false) barcodeLauncher.launch(options) あとはbuild.gradleに、以下。 implementation 'com.journeyapps:zxing-android-embedded:4.3.0' また、Manifestに、以下を追加。 <activity android:name="com.journeyapps.barcodescanner.CaptureActivity" android:screenOrientation="fullSensor" tools:replace="screenOrientation" />
- 投稿日:2021-12-03T17:19:48+09:00
【Flutter】バッテリーの充電状態を検知する方法
本記事はFlutter Advent Calendar2021 3日目の記事です。 スマホを使うにあたって、切っても切れない関係にあるのがバッテリーですよね。 今回はFlutterでバッテリーの充電状態を検知する方法を提供するパッケージ、 battery_plusを紹介します。 こちらについて日本語での解説記事が見つからないため、 今回紹介しようと考えた次第です。 本記事では、 buttery_plusパッケージでできること 実装サンプル を記載します。 参考になれば幸いです。 buttery_plusパッケージでできること buttery_plusパッケージでできることは以下の3つです。 充電量(%)を取得する 充電状態(後述)の変化を検知する パワーセーフモードか否かを取得する やれることはシンプルですね。 一つ一つ解説していきます。 充電量(%)を取得する batteryLevelのメソッドで現在の充電量を%で取得します。 このメソッドの返り値はFuture型なのは注意です。 //Batteryのインスタンスを取得 battery = Battery(); //充電量をawaitで取得 int batteryLevel = await battery.batteryLevel; 充電状態の変化を検知する 充電状態(BatteryState)はenumで定義されており、 以下の4つの状態があります。 charging:充電中の状態 discharging:充電されておらず、バッテリーが減っている状態 full:バッテリーが満タンの状態 unknown:充電状態が検知できない状態 これらの変化を検知するメソッドonBatteryStateChangedが用意されています。 返り値がStream型なのがポイントです。 (使用例は実装サンプルをご覧ください。) パワーセーフモードか否かを取得する スマホがパワーセーフモードか否かを取得します。 取得はisInBatterySaveModeのメソッドで行います。 これも返り値がFuture型なのに注意です。 //Batteryのインスタンスを取得 battery = Battery(); //パワーセーブモードか否かをawaitで取得 bool isInPowerSaveMode = await _battery.isInBatterySaveMode; 実装サンプル 実装サンプルを紹介します。 公式のExampleをベースに、 不要部分の削除、日本語のコメントを追加したものとなります。 まず最初にpubspec.yamlに以下のコードを追加し、pub getを行なってください。 (バージョンはパッケージの紹介ページを見て最新に更新してください。) battery_plus: ^2.0.1 サンプルコード import 'dart:async'; import 'package:flutter/material.dart'; import 'package:battery_plus/battery_plus.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return const MaterialApp( title: 'Battery Plus Demo', home: BatteryPage(title: 'Battery Plus Demo'), ); } } class BatteryPage extends StatefulWidget { const BatteryPage({Key? key, this.title}) : super(key: key); final String? title; @override _BatteryPageState createState() => _BatteryPageState(); } class _BatteryPageState extends State<BatteryPage> { //バッテリーのインスタンスを取得 final Battery _battery = Battery(); //バッテリーの状態(充電中等)をStateとして保持 BatteryState? _batteryState; //StreamSubscriptionで監視する(初期化はinitStateで行う) StreamSubscription<BatteryState>? _batteryStateSubscription; @override void initState() { super.initState(); //Batteryの状態の変化を検知し、setStateするように設定 _batteryStateSubscription = _battery.onBatteryStateChanged.listen((BatteryState state) { setState(() { _batteryState = state; }); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Plugin example app'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ //バッテリーの状態を表示 Text('$_batteryState'), ElevatedButton( onPressed: () async { //充電残量の取得 final batteryLevel = await _battery.batteryLevel; showDialog<void>( context: context, builder: (_) => AlertDialog( content: Text('Battery: $batteryLevel%'), actions: <Widget>[ TextButton( onPressed: () { Navigator.pop(context); }, child: const Text('OK'), ) ], ), ); }, child: const Text('Get battery level'), ), ElevatedButton( onPressed: () async { //パワーセーブモードか否かの取得 final isInPowerSaveMode = await _battery.isInBatterySaveMode; showDialog<void>( context: context, builder: (_) => AlertDialog( content: Text('Is on low power mode: $isInPowerSaveMode'), actions: <Widget>[ TextButton( onPressed: () { Navigator.pop(context); }, child: const Text('OK'), ) ], ), ); }, child: const Text('Is on low power mode')) ], ), ), ); } @override void dispose() { super.dispose(); //Subscriptionのストップ if (_batteryStateSubscription != null) { _batteryStateSubscription!.cancel(); } } } ポイント StreamSubscription<BatteryState>で充電状態の検知を行なっているのがポイントです。 listenでリスナーを設定し、 充電状態が変化するたびにsetStateされるように設定しています。 disposeでSubscriptionを停止するのを忘れないようにしましょう。 実行時の注意点 このアプリを実行するときは、flutter run --releaseで実行することをお勧めします。 iPhoneでDebugモードで実行すると接続コードを抜いた瞬間にアプリが落ちてしまいます。 releaseモードで実行することで、接続コードを抜いてもアプリを使用することができます。 充電コードの抜き差しを確認するには必要テクニックですので、ぜひやってみてください。 GitHub 本記事で紹介したサンプルコードはGitHubにて公開しています。 気軽にクローンいただければと思います。 まとめ 今回はFlutterでバッテリー周りの情報を提供するパッケージ、battery_plusの紹介を行いました。 buttery_plusパッケージでできること 実装サンプル を記載しました。 バッテリーアプリを作成することはなかなかないかもしれませんが、 バッテリーの数値を表示する、とか、 充電されている時に〜するとか、 実装できることを知っているだけでも アプリ制作の幅が広がって良いかと思います。 Flutterのアドベントカレンダーで3日目がちょうど空いていたので書いた記事でしたが、いかがでしたでしょうか。 本記事が参考になれば幸いです。
- 投稿日:2021-12-03T11:23:21+09:00
Hiltを使ってみよう
今回の記事はレコチョク Advent Calendar 2021の7日目の記事となります。 はじめに はじめまして!株式会社レコチョクでAndroid開発を担当しています、入社2年目の杉山です! 音楽が好きで普段からよくライブに行っています。特にアイドルが好きでメジャーアイドルからインディーズアイドルまで様々なグループを見たりしています! そんな僕からはAndroid開発に参加するようになって触れたDIライブラリのHiltについて記事をまとめたいと思います! Hiltって、DIって何? そもそもHiltってなんぞやって話になりますが、HiltはAndroid用の依存関係をインジェクションするライブラリになります。いわゆるDIライブラリと呼ばれるものです。 DIって? ある程度の規模のプロジェクトになると、「あるクラスAの実装を行う際、他のクラスBのオブジェクトが必要」という場面に遭遇することが増えます。この関係を依存関係と呼びます。 するとAの内部でBを呼び出す必要が出てきます。すると関係は固定的になりAがBを利用していることが外部からわかりにくくなります。 これを解消するために依存性注入(Dependency Injection)いわゆるDIと呼ばれるものが必要になります。その場合、Aの内部にBを直接記述せず、メソッドの引数などの形で受け取って呼び出す形にします。これにより、AとBの依存関係は外部から任意に指定、変更することができるようになり、例えばBと同じインターフェースを実装したCを与えたり、Bをモックとして与えてAのみを対象とした単体テストができるようになったりします。 Hiltとは Daggerの上に構築された新しい依存性注入ライブラリであり、Androidアプリでの仕様を簡素化してくれるものになります。 Daggerとはオブジェクト間の依存関係を管理するコードを自動生成してくれるツールになります。 アノテーションを使用して、クラスの依存関係を宣言し、依存関係を満たす方法を指定していれば、ビルド時に自動的に依存関係を管理してくれます。HiltはDaggerを元に更に簡素化して使用できるようにしたものになります。実際には、オブジェクトの作成方法とオブジェクトを挿入する場所を定義するだけで使用することができるようになっています。 プロジェクト内のすべてのAndroidクラスにコンテナを提供し、そのライフサイクルを自動で管理することで、アプリケーションでDIを行うための標準的な方法を提供してくれます。(公式ドキュメントより) 今回はそのHiltに関して導入と実際に使ってみた部分に関してまとめていきたいと思います! Hiltを導入 Hiltを使用するにあたって、まずGradleに必要なプラグインを追加します。 * ルート直下のGradle buildscript { dependencies { classpath("com.google.dagger:hilt-android-gradle-plugin:2.40.2") } } appフォルダ配下のGradle plugins { id('dagger.hilt.android.plugin') } dependencies { implementation("com.google.dagger:hilt-android:2.40.2") kapt("com.google.dagger:hilt-android-compiler:2.40.2") } これで導入は完了です。実際に使ってみます!今回はToDoアプリのタスク管理する部分に導入しようと思います Hiltを使用するアプリケーションには次のようなアノテーションを付与します。 @HiltAndroidApp class ToDoApplication: Application() {} これでこのアプリケーションではHiltを使用することを宣言できました。 アプリケーションクラスはAndroidManifestで指定します … <application android:name=".ToDoApplication" … Hiltを対応させるアクティビティとフラグメントに対しては次のようなアノテーションを付与します。 @AndroidEntryPoint class AddEditToDoActivity : AppCompatActivity() { フラグメントのみ付与してアクティビティに付与しないとエラーが出てしまうので注意です! ここまで出来たらHiltのモジュールを作成します。 @Module @InstallIn(SingletonComponent::class) object ToDoModule { @Provides @Singleton fun provideToDoDatabase( @ApplicationContext context: Context ): ToDoDataBase { return ToDoDataBase.getInstance(context) } } まずは@Moduleと@InstallIn()を付与します。@ModuleでHiltモジュールにします。@InstallIn()でModuleをどのHiltコンポーネントにインストールするかを指定します。各メソッドには@Providesを付与し、一つだけ作成するようにする(アプリ内で共通の一つ)にする場合は@Singletonを付与します。 Contextを定義に使用する場合は@ApplicationContextを利用することで定義が可能になります。 ビューモデルをHilt対応したものに修正します。 @HiltViewModel class AddEditToDoViewModel @Inject constructor( private val toDoRepository: ToDoRepository ): ViewModel() { このとき@Injectというアノテーションを使用しています。それによりHiltが自動的に対象インターフェースの実装クラスをDIしてくれます。 ビューモデルをフラグメントに持たせる場合以下のように宣言します。 @AndroidEntryPoint class AddEditToDoFragment : Fragment() { private val addEditToDoViewModel: AddEditToDoViewModel by viewModels() 今回は以上によりデータベース周りの依存関係をHilt対応させることが出来ました。 まとめ 今回はHiltを業務で使ってみたので、Hiltの初歩的な内容についてまとめてみました。 実際に業務で使っていると、クラス間の関係性がわかりやすくなり、テストがしやすいというメリットがあったので今後も使っていきたいと思います! 明日のレコチョク Advent Calendar 2021は8日目 デザイナーとスクラムです。お楽しみに! この記事はレコチョクのエンジニアブログの記事を転載したものとなります。
- 投稿日:2021-12-03T10:22:45+09:00
【Jetpack Compose】Stateと再Composeの仕組みを検証してみた
この記事はandfactoryアドベントカレンダー2021の3日目の記事です? はじめに Jetpack Composeを書いていて、Stateを更新すると画面が更新されると思うのですが、その際に再Compose処理が発生します。再Compose処理はドキュメントによると「入力が変化している」部分で発生し、そうでない部分は回避してくれるようです。 ですが、じゃあ具体的にどのように書いたら回避されて、どう書いていたら回避されないのかがいまいち理解できていませんでした。 特に、ViewModelで保持するFlowやLiveDataのObserve場所です。自分は基本的に、ViewModelの値をComposeの上位でobserveして、他の下位Composeはシンプルな値を引数をとるように実装しています。 その場合、ViewModelの値が変わるたびに全体が再Composeされるのか、よしなになってくれるのかがわからなかったので、今回実際にコードを書いて試してみることにしました? 再Composeについて 再Composeについての説明は公式のドキュメントにまとまっています。 再Composeとは、値が変化したときなどに状態を画面に反映させるためComposeが再構築されることです。 画面の更新には必要不可欠の処理ですが、この再Composeの範囲が大きいとパフォーマンスが悪くなります。 通常、再コンポジションは State オブジェクトの変更によってトリガーされます。Compose はそうした変更をトラッキングし、特定の State を読み取る Composition 内のすべてのコンポーザブルと、スキップできない呼び出し対象コンポーザブルを実行します。 再コンポジションの際にコンポーザブルが前回のコンポジションのときと異なるコンポーザブルを呼び出した場合、Compose はどのコンポーザブルが呼び出され、どのコンポーザブルが呼び出されなかったかを識別し、両方のコンポジションで呼び出されたコンポーザブルについては、入力が変化していなければ再コンポジションを回避します。 色々試してみる 今回使うコード 今回試すにあたって、こんな感じのコードで試してみました。 MainScreen.kt @ExperimentalMaterialApi @Composable fun MainScreen(viewModel: MainViewModel = viewModel()) { val cards by viewModel.cards.collectAsState() Body(cards, onClickCard = viewModel::onClickCard) } @ExperimentalMaterialApi @Composable private fun Body(cards: List<CardData>, onClickCard: (id: Int) -> Unit) { Column(modifier = Modifier.fillMaxSize()) { LazyColumn(modifier = Modifier.fillMaxSize()) { item { Header("State with Recompose") } item { Spacer(modifier = Modifier.height(16.dp)) } cards.forEach { item { CardCompose( modifier = Modifier .fillMaxWidth() .padding(horizontal = 32.dp), card = it, onClick = onClickCard, ) Spacer(modifier = Modifier.height(24.dp)) } } } } } @Composable private fun Header(text: String) { Box( contentAlignment = Alignment.Center, modifier = Modifier .height(150.dp) .fillMaxWidth(), ) { Text(text, fontSize = 22.sp) } } @ExperimentalMaterialApi @Composable private fun CardCompose(modifier: Modifier = Modifier, card: CardData, onClick: (id: Int) -> Unit) { Card( modifier = modifier.wrapContentHeight(), shape = RoundedCornerShape(8.dp), elevation = 4.dp, onClick = { onClick(card.id) }, ) { Box(Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) { Image( painter = rememberImagePainter(card.url), contentDescription = null, modifier = Modifier.size(80.dp) ) Column( modifier = Modifier .wrapContentHeight() .padding(8.dp) ) { Text(card.title, fontSize = 22.sp) Text(card.description, fontSize = 16.sp, softWrap = true) } } if (!card.enable) { Box( modifier = Modifier .matchParentSize() .background(Color(0x88FFFFFF)) ) } } } } MainViewModel.kt data class CardData( val id: Int, val url: String, val title: String, val description: String, val enable: Boolean, ) class MainViewModel : ViewModel() { private val _cards = MutableStateFlow( (1..20).toList().map { CardData( id = it, url = "https://placehold.jp/3d4070/ffffff/80x80.png?text=Image", title = "Card $it", description = "description description", enable = true, ) } ) val cards: StateFlow<List<CardData>> get() = _cards fun onClickCard(id: Int) { viewModelScope.launch { val update = cards.value.map { it.copy( enable = if (it.id == id) { !it.enable } else { it.enable } ) } _cards.emit(update) } } } Cardをリスト表示するサンプルです。ViewModelにカードのリストデータをFlowで保持しています。 そしてタップするとFlowを更新してカードの活性非活性をトグル表示しています。 Composeの関係性は下記のような構成になっています。 検証1 まず最初に、ViewModelの値を上位のComposeでobserveしてみます。 @ExperimentalMaterialApi @Composable fun MainScreen(viewModel: MainViewModel = viewModel()) { val cards by viewModel.cards.collectAsState() Body(cards, onClickCard = viewModel::onClickCard) } @ExperimentalMaterialApi @Composable private fun Body(cards: List<CardData>, onClickCard: (id: Int) -> Unit) { // ... } @ExperimentalMaterialApi @Composable private fun CardCompose(modifier: Modifier = Modifier, card: CardData, onClick: (id: Int) -> Unit) { // ... } こんな感じです。cardリストデータはMainScreenでobserveし、値を下位のComposeに渡しています。下位のComposeはStateではなく Card 型を受け取っています。 この場合どこが再Composeされるのでしょうか。 再Composeの判定にあたっては SideEffect を使ってログ表示してみました。 SideEffect はCompositionが成功するたびに呼び出されるやつです。 @ExperimentalMaterialApi @Composable fun MainScreen(viewModel: MainViewModel = viewModel()) { val cards by viewModel.cards.collectAsState() SideEffect { Log.d("MainScreen", "composition!") } Body(cards, onClickCard = viewModel::onClickCard) } これでComposeが呼び出されるたびにログが表示されます。 まずは初回起動のログ表示です。 普通に全部の箇所のログが表示されますね。 D/MainScreen: composition! D/Body: composition! D/Header: composition! D/CardCompose: composition! card id = 1 D/CardCompose: composition! card id = 2 D/CardCompose: composition! card id = 3 D/CardCompose: composition! card id = 4 D/CardCompose: composition! card id = 5 D/CardCompose: composition! card id = 6 スクロールしていくと、CardComposeのログが増えていきます。カードデータは全部で20件なのですが、ある程度スクロールすると破棄されて、また画面に入るたびに再Composeが走るようでした。 では早速カードをタップして状態を更新してみます! D/MainScreen: composition! D/Body: composition! D/CardCompose: composition! card id = 2 カード2をタップして表示を切り替えてみたときのログです。 結果はFlowをObserveしているところからCardまで再Composeが発生していました。 ですが、CardComposeのログが1件して表示されていません。タップして状態が変化したCardComposeのみが再Composeされており、他のComposeは走ってませんでした? 赤色にしている部分が再Composeが走ったところです。Bodyは再Composeされてますが、Headerや関係のないCardは再Composeされていません。 正直、僕はMainScreenから値の変更を通知した場合はまるごと再Composeされるとばかり思っていました。 いや、これめっちゃ賢いぞ。。? 検証2 もう一つ検証してみます。 今はCardComposeは単一のComposeですが、この中のImageの部分やテキストの部分を適当に階層分けしてみた場合、上のComposeの状態が変更されて、下位は更新されないケースはどうでしょうか。 こんな感じで適当に階層を増やしてみました。 @Composable private fun CardThumbnail(modifier: Modifier = Modifier, url: String) { Image( painter = rememberImagePainter(url), contentDescription = null, modifier = modifier ) } @Composable private fun CardTitle(title: String) { Text(title, fontSize = 22.sp) } @Composable private fun CardDescription(description: String) { Text(description, fontSize = 16.sp, softWrap = true) } url や title description は全部Cardの値です。タップされたときに更新されるのはenableなので値の変更はないのですが、対象のCardは更新されているわけで、、なんとなくこの子要素たちも再Composeされそうな予感がしていますがどうでしょうか。 同様に新しい子要素にもSideEffectとログを仕込んで、2つ目のカードをタップしてみました。 結果はこちら! D/MainScreen: composition! D/Body: composition! D/CardCompose: composition! card id = 2 おおすごい!値が変わっていない部分は再Composeが走りません。 検証3 今度はStateの値の渡し方をちょっと変えてみます。カードの値をStateのまま渡した場合はどうでしょうか。 階層構造は変更しませんが、MainScreenからBodyへの値の渡し方だけ変えてみます。 @ExperimentalMaterialApi @Composable fun MainScreen(viewModel: MainViewModel = viewModel()) { val cards = viewModel.cards.collectAsState() Body(cards, onClickCard = viewModel::onClickCard) } @ExperimentalMaterialApi @Composable private fun Body(cards: State<List<CardData>>, onClickCard: (id: Int) -> Unit) { Column(modifier = Modifier.fillMaxSize()) { LazyColumn(modifier = Modifier.fillMaxSize()) { item { Header("State with Recompose") } item { Spacer(modifier = Modifier.height(16.dp)) } // ここでState.valueを呼び出す cards.value.forEach { item { CardCompose( modifier = Modifier .fillMaxWidth() .padding(horizontal = 32.dp), card = it, onClick = onClickCard, ) Spacer(modifier = Modifier.height(24.dp)) } } } } } } この状態でカード2をタップしてみます。 結果はこちら。 D/CardCompose: composition! card id = 1 D/CardCompose: composition! card id = 2 D/CardCompose: composition! card id = 3 D/CardCompose: composition! card id = 4 D/CardCompose: composition! card id = 5 D/CardCompose: composition! card id = 6 な、なるほど。。たしかにLazyColumnのなかで cards.value しているのでこうなるんですね。必要ないCardの再Composeも走ってしまっています。 ではBodyでの受け取り方を下記のようにしてみたらどうでしょうか。 @ExperimentalMaterialApi @Composable private fun Body(cardsState: State<List<CardData>>, onClickCard: (id: Int) -> Unit) { val cards = cardsState.value Column(modifier = Modifier.fillMaxSize()) { LazyColumn(modifier = Modifier.fillMaxSize()) { item { Header("State with Recompose") } item { Spacer(modifier = Modifier.height(16.dp)) } cards.forEach { item { CardCompose( modifier = Modifier .fillMaxWidth() .padding(horizontal = 32.dp), card = it, onClick = onClickCard, ) Spacer(modifier = Modifier.height(24.dp)) } } } } } ログは割愛しますが、期待した通りの結果となりました。ObserveしているMainScreenでは再Composeが走らずに、Bodyからになっています。 State.valueをしたComposeから変更検知となるのでMainScreenの再Composeはスキップすることができていますが、、正直わざわざStateの形式で引数に渡していく必要性はないかもですね。 まとめ State.valueを呼び出したComposeは再Composeされる 引数の値が変わっていれば再Compose。自身の引数が変わっていなければ、親が再Composeされていても自分は再Composeされない! ViewModelの値は親のComposeでObserveして問題なさそう 再Composeの仕組みが自分で検証してみて理解することができました? 今まで、親のComposeが再Composeされると子も再Composeされるのかとばかり勘違いしていたのですが、もっと賢い動きをしていました(笑) この基本的な挙動に加えて、LazyColumnなどではkeyを使うことで再Composeの最適化などもできるようですが、普通に使ってても賢く更新処理してくれてました。まずは普通に実装して、パフォーマンス改善したいときなどに検討してみれば良さそうです。
- 投稿日:2021-12-03T09:06:14+09:00
コマンドでdetektをかける
gradleで入れるのが面倒だったのでとりあえずCLI版を入れてしまう curl -sSLO https://github.com/detekt/detekt/releases/download/v1.18.1/detekt-cli-1.18.1-all.jar 以下のコマンドでテキスト形式で結果を出力できる java -jar detekt-cli-1.18.1-all.jar -r txt:detekt.txt
- 投稿日:2021-12-03T09:04:42+09:00
コマンドでcheckstyleをかける
上からcheckstyleのjarをダウンロードする 循環複雑度をシュッとかけるためのxml <?xml version="1.0"?> <!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd"> <module name = "Checker"> <module name="TreeWalker"> <module name="CyclomaticComplexity"> <property name="max" value="7"/> </module> </module> </module> おもむろに実行してみる java -jar <PATH>/checkstyle-9.0.1-all.jar -c <上のXML>.xml -o checkstyle_result.txt **/*.java