- 投稿日:2021-05-09T21:29:02+09:00
【C#】Androidアプリの画面遷移
前回書いたC#のandroidアプリの作成方法の続き(補足)として、今回は一つのアプリ内で別のページを表示する方法について書いていこうと思います。 基本的には、デフォルトで生成されたコードを読み解けばわかる内容ではあるので、読まなくてもわかるという方はスルーしてください(笑) 下記が前回書いた記事ですので、今回の内容がわからない、もしくは単純に興味があるという方は見ていただけるとありがたいです。 https://qiita.com/NagaJun/items/87f126249028fa2b23a0 実装 前回の記事で書いた内容(プロジェクト作成時に自動で作成されるコード)を前提として、コードを書いていきます。 前提としては、/Resource/Layout/フォルダー下にページデザイン用のファイルであるcontent_main.xmlとactivity_main.xmlがあり、動きを制御するコードとしてMainActivity.csがあります。 今回のサンプルを作成するために以上の三つのファイルを修正します。 修正内容は以下の通りです。 activity_main.xml <?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout 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" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffffe0"> <TextView android:text="activity_main_page" android:textAppearance="?android:attr/textAppearanceLarge" android:layout_width="match_parent" android:layout_height="match_parent" android:minWidth="25px" android:minHeight="25px" android:id="@+id/textView1" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="change view content_menu" android:id="@+id/btn_show_content_main_page" android:layout_marginTop="@dimen/design_bottom_navigation_active_item_max_width" > </Button> </android.support.design.widget.CoordinatorLayout> content_main.xml <?xml version="1.0" encoding="utf-8"?> <RelativeLayout 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" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffd700" > <TextView android:text="content_main" android:textAppearance="?android:attr/textAppearanceLarge" android:layout_width="wrap_content" android:layout_height="wrap_content" android:minWidth="25px" android:minHeight="25px" android:id="@+id/textView1" android:layout_marginBottom="35.5dp" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="change view activity" android:id="@+id/btn_show_activity_main_page" android:layout_marginTop="@dimen/design_bottom_navigation_active_item_max_width" > </Button> </RelativeLayout> MainActivity.cs //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ public class MainActivity : AppCompatActivity { /// <summary>content_mainページ表示用ボタン</summary> private Button BtnShowContent { get; set; } /// <summary>activity_mainページ表示用ボタン</summary> private Button BtnShowActivity { get; set; } /// <summary>アプリ起動時</summary> /// <param name="savedInstanceState"></param> protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); Xamarin.Essentials.Platform.Init(this, savedInstanceState); // 初期ページを表示 SetContentView(Resource.Layout.activity_main); // contentページ表示ボタン BtnShowContent = FindViewById<Button>(Resource.Id.btn_show_content_main_page); BtnShowContent.Click += BtnShowContent_Click; } /// <summary>contentページ表示イベント</summary> /// <param name="sender"></param> /// <param name="e"></param> private void BtnShowContent_Click(object sender, EventArgs e) { SetContentView(Resource.Layout.content_main); // activityページ表示ボタン BtnShowActivity = FindViewById<Button>(Resource.Id.btn_show_activity_main_page); BtnShowActivity.Click += BtnShowActivity_Click; } /// <summary>activityページ表示イベント</summary> /// <param name="sender"></param> /// <param name="e"></param> private void BtnShowActivity_Click(object sender, EventArgs e) { SetContentView(Resource.Layout.activity_main); // contentページ表示ボタン BtnShowContent = FindViewById<Button>(Resource.Id.btn_show_content_main_page); BtnShowContent.Click += BtnShowContent_Click; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 不要なコードは全て省いてあるため、必要に応じて調整してください。 activity_main.xmlとcontent_main.xmlに関しては、今回サンプルとして記述した内容をそのままコピペしてもらえば大丈夫かと思います。 元のactivity_main.xmlには<include Layout=~>という記述があったかと思いますが、その記述でcontent_main.xmlの内容を読み込んでいるみたいなので、その記述は消してあります。(画面遷移を見たいので、ページを入れ子で読み込ませてあるとわかりにくいため) 今回、MainActivity.csに対して記述した内容は、各ページのボタンクリック時にSetContentView()で別ページを表示するようにしてあります。 ここで注意点ですが、FindViewById()で取得できるオブジェクト(今回の場合だとButton)は処理時に画面上に存在しているものに限られるため、OnCreate()メソッド内で初期画面に表示されないbtn_show_activity_main_pageボタンを取得することはできません。 また、画面が切り替わると取得済みのボタンのコントロールが失われるため、ページを切り替えるたびにオブジェクトの取得とイベントの設定を行う必要があります。(設定したはずのイベントも失われます) 動作確認 上記の修正を加えたら、前回同様の手段でデバッグを行います。 挙動は単純で下記画像の様な画面が立ち上がり、CHENGE VIEW~ボタンを押すたびに画面が切り替わります。 背景色と文字列が変わっているだけなので、一見、プロパティをいじっているだけにも見えますが(笑) もっとわかりやすい変化を見たいという方は.xmlファイルを修正してみるとよいかと思います。 終わりに 今回調べていて知ったのですが、.xmlファイルに記述する内容に関しては、android studioでアプリを作成する場合と基本的に同じなんですね(当然といえば当然ですが) C#のandroidアプリより、android studioの情報の方が、ネット上の先人の知恵が多いので助かります(笑)
- 投稿日:2021-05-09T18:08:33+09:00
AndroidでAmazon Chime SDKを使ってみた(アプリ実装編)
はじめに 前回のつづきです。今回は実際にAndroidで実装していきます。開発にはamazon-chime-sdk-androidを用います。 SDKの導入方法 基本的には公式のガイドラインがあるのでその内容通りにセットアップすればOKです。 まずはバイナリファイルの最新のreleaseのもの(amazon-chime-sdkとamazon-chime-sdk-media)をダウンロードします。ダウンロードしたファイルは/app/libsにおいておきます。 次にroot projectのbuild.gradleに以下の設定を書き加えます。 build.gradle ... allprojects { repositories { jcenter() flatDir { dirs 'libs' } } } ... あとはappの中のbuild.gradleのdependencies配下に以下を書き加えます。 build.gradle implementation(name: 'amazon-chime-sdk', ext: 'aar') implementation(name: 'amazon-chime-sdk-media', ext: 'aar') また、compileOptionsが以下のようになっているかも確認しておいてください。 build.gradle compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } この設定を行えばSDKの導入は完了です。 アプリについて アプリの外観は↓のような感じです。ソースコードはこちらで公開しています。 かなり簡素に作ったのであまり実用的ではありませんが、最低限のビデオ通話の機能を使うことができます。 今回はルームのIDを固定してルームを作成して(前回つくったサーバーを使用しています)、その中でユーザー同士の通話機能を実装しています。 ちなみにAmazon Chime SDKの機能をフルに使ったサンプルコードは公式のデモアプリで公開されているので、そちらを参考にするといいと思います。 Amazon Chimeとの通信を開始する ルームとユーザーの作成を行うAPIを叩くための関数をcoroutineとgsonを使って実装しました。 private val gson = Gson() private val logger = ConsoleLogger(LogLevel.DEBUG) private val TAG = "apiRequest" suspend fun joinMeeting( ioDispatcher: CoroutineDispatcher, meetingUrl: String, meetingId: String?, attendeeName: String? ): String? { return withContext(ioDispatcher) { val url = if (meetingUrl.endsWith("/")) meetingUrl else "$meetingUrl/" val serverUrl = URL("${url}join") try { val response = StringBuffer() with(serverUrl.openConnection() as HttpURLConnection) { requestMethod = "POST" doInput = true doOutput = true val body = gson.toJson(mutableMapOf( "title" to meetingId, "name" to attendeeName )) setRequestProperty("Content-type", "application/json; charset=utf-8") outputStream.use { val input = body.toByteArray() it.write(input, 0, input.size) } BufferedReader(InputStreamReader(inputStream)).use { var inputLine = it.readLine() while (inputLine != null) { response.append(inputLine) inputLine = it.readLine() } it.close() } if (responseCode == 200) { response.toString() } else { logger.error(TAG, "Unable to join meeting. Response code: $responseCode") null } } } catch (exception: Exception) { logger.error(TAG, "There was an exception while joining the meeting: $exception") null } } } joinMeetingで返ってきたレスポンスを使ってAmazon Chimeとの通信を行います。このあたりの実装はAmazon Chime SDKを使うことでWebRTCなどの難しい技術を理解せずにアプリケーションを実装することができます。以下はそのコード例です(公式ページより拝借) // joinMeeting()で返ってきたレスポンスのデータ構造 data class JoinMeetingResponse( @SerializedName("JoinInfo") val joinInfo: MeetingInfo) data class MeetingInfo( @SerializedName("Meeting") val meetingResponse: MeetingResponse, @SerializedName("Attendee") val attendeeResponse: AttendeeResponse) data class MeetingResponse( @SerializedName("Meeting") val meeting: Meeting) data class AttendeeResponse( @SerializedName("Attendee") val attendee: Attendee) // Gsonを使ってレスポンスデータをパースする val joinMeetingResponse = Gson().fromJson( response.toString(), JoinMeetingResponse::class.java ) val configuration = MeetingSessionConfiguration( CreateMeetingResponse(joinMeetingResponse.joinInfo.meetingResponse.meeting), CreateAttendeeResponse(joinMeetingResponse.joinInfo.attendeeResponse.attendee) ) // DefaultMeetingSessionを実行することでAmazon Chimeとの通信が実行できる val meetingSession = DefaultMeetingSession(configuration, ConsoleLogger(), applicationContext) meetingSessionはviewModelで管理するといいと思います。あとはmeetingSessionで以下を実行するとビデオ通話が開始されます。(これも公式より抜粋) val audioVideo = meetingSession.audioVideo // Start audio and video clients. audioVideo.start() // Start receiving remote video. audioVideo.startRemoteVideo() // Start sending local video. audioVideo.startLocalVideo() // Switch camera for local video between front and back. audioVideo.switchCamera() ビデオタイルを描画する ビデオタイル(ビデオチャットアプリの各カメラ映像)をビューに表示させるためにVideoRenderViewを使います。このviewへのビデオ映像のbindを行うために.bindVideoView(videoRenderView, tileId)を実行します。以下のような感じで実装しました。 audioVideo.addVideoTileObserver(this) private fun showVideoTile(tileState: VideoTileState) { val activity = activity as Context val defaultVideoRenderView = DefaultVideoRenderView(activity) val layout = view?.findViewById<LinearLayout>(R.id.meeting_layout) layout?.addView(defaultVideoRenderView, ViewGroup.LayoutParams(500, 500)) audioVideo.bindVideoView(defaultVideoRenderView,tileState.tileId) } override fun onVideoTileAdded(tileState: VideoTileState) { showVideoTile(tileState) } override fun onVideoTileRemoved(tileState: VideoTileState) { audioVideo.unbindVideoView(tileState.tileId) } 簡単のため、viewの作成のところ(showVideoTile)は結構雑な実装をしています。onVideoTileAddedやonVideoTileRemovedは最初にオブザーバの登録(ここではaudioVideo.addVideoTileObserver(this))を行うことで呼ばれるようになります。 ルームに新しくユーザーが参加するとonVideoTileAddedが呼ばれてそのtileIdが取得できます。この情報を使ってdefaultVideoRenderViewをbindさせることで、ユーザーがルームに入るたびにビデオタイルも追加する形のものが実装できます。 さいごに Androidでビデオチャットアプリを作るのは(そもそも私自身がAndroidの実装経験が乏しいのも含めて)難しいイメージを持っていたのですが、Amazon Chime SDKを使うと難しい通信周りを意識せずにUIへの実装のみに集中してビデオチャットアプリを作ることができました。 とはいえほんの一部の機能を使っただけなので、他の機能(特にCustomVideo)も触ってみたいですね。
- 投稿日:2021-05-09T16:08:49+09:00
【Android】UNITテスト集
サーバーサイドのバックエンドでも Java, Kotlin での実装はされている。 しかし、Android は OS 特有の機能が多いのでバックエンドの Java, Kotlin にはない実装をすることもある。 Android で重要になりそうな UNITテスト(テスト自動化)のまとめを作ってみた(今後も更新予定) ウィジェット SDKバージョン MVVM 最後に 機能の実装だけではなく、テストをしっかり書くことも重要!
- 投稿日:2021-05-09T15:29:06+09:00
[Unity]WebGL出力をスマホのブラウザで実行する
Unity 上で作成したアプリは WebGL で出力でき、ブラウザで実行できます。が、公式マニュアルには Mobile devices are not supported by Unity WebGL. と、スマホのブラウザには対応していない旨が記載されています。 が、実際のところは、動きます。公式にサポートされていないだけで、ほぼ、動くんですよね。 以下は、「動かないものを動かす」というよりは、「動かない風な挙動を取り除く」という、ほとんど表層のテクニックになります。 なお Unity 2021.1.5f1 を使用していますが、Web Assembly が有効になった 2018.2 以降、基本的な状況は同じと思われます。また、今回はレガシーレンダリングで試していますが、URPでも動くはずです。ただしURPではいくつかの制約を回避する必要があるので、動作実験をするなら現時点ではレガシーレンダリングがよいです。 ステップ1.普通にビルドする まずは任意のプロジェクトでビルドし、PC上のブラウザで動作するのを確認しましょう。 それをスマホで実行するのは、自分でHTTPサーバを立てるのが便利でしょう。多少のネットワークの知識が必要ですが、ここでは省略します。 スマホで実行すると こんな表示が出ます。まあ、これを出されたら「ああダメなんだな」って思わされますよね。待っているとこのメッセージは消えるんですが、画面は真っ黒なままです。 なお、ここのビルドで生成されたファイルをステップ3で使用するので、スマホで表示するテストはしなくてもいいですがビルドだけはやっておきましょう。 ステップ2. Graphics API を WebGL 1 にする スマホで画面が出ないのは Graphics API に WebGL 2.0 が選択されているからです。 Project Settings を開き、プレイヤーの この「自動グラフィックスAPI」のチェックを外し、 WebGL 1 を追加し、 WebGL 2 を削除してこんな状態にします。 実はこれでもう、スマホで画面が出るようになります。ただし WebGL 1 にすることでいくつかの描画アルゴリズムを利用できなくなります。 WebGL 1 と WebGL 2 の違いは以下にまとめられています。 ステップ3. WebGLTemplates を設定する 警告メッセージ "WebGL builds are not supported on mobile devices." を出さないようにしましょう。 Assets/WebGLTemplates というフォルダを作成します。これは予約フォルダ名で、HTMLのテンプレートを格納できます。 さらに MyTemplate(なんでも良いです)というフォルダを作り、その下に、ステップ1のビルドで生成された - index.html - Templates(フォルダ) をコピーして配置します。 配置した index.html をテキストエディタで開き、下のほうの unityShowBanner('WebGL builds are not supported on mobile devices.'); という行を削除します。 Project Settings の Player に、配置したフォルダが WebGLテンプレート のところで選べるようになっているはずです。MyTemplate を選択します。 これで、次のビルドからモバイルでの警告が出なくなります。 なお、特に圧縮設定を変更した場合に、 index.html がそれに対応している必要があるのに注意してください。問題が起きたらこのステップをやりなおしましょう。 完了 以上です。やってることはほぼ、グラフィクスAPIを WebGL 1 にするだけですね。 おまけ:サーバ設定 なお、HTTPサーバに応じた対応が必要になるケースがあるようです。 私のテストの範囲ではこの対応をすることなく動作させることができましたが。 おまけ2:メモリ設定 スマホブラウザおける問題として挙げられるのは、使用可能なメモリ容量です。 この記事にメモリ上限の設定方法があります。 PlayerSettings.WebGL.emscriptenArgs = "-s WASM_MEM_MAX=512MB"; メモリ上限を定めることで明確に「動作しない」ことをエンドユーザに伝えることができます(的なことがそこに書いてある)。 まとめ 動くと言っても、やっぱり非対応は非対応です。そんな、公式にはサポートされていないものとどう向き合うか、という話ですが、ビルドしたものが動作してしまえば、それは安定していると言えます。であるなら、商売に利用することも検討していいと思うんですよね。スマホの性能も上がってきてますし。もちろん性能には幅があるので、おまけ2で触れたような、動かない条件を定めて通知できるようにすることが肝要と思います。また、継続的にUnityの最新バージョンに追随する計画がある場合は、慎重になったほうがいいでしょう。 ちょっとしたゲームの試作を知人に見てもらう、のようなケースにおいては、今回の対応でスマホで動作できるようにしておくのはとても有効だと思います。 P.S. 実は、今回の私の調査はサイズ削減に大半の労力が割かれていたんですが、情報単位は小さい方がよかろうということで、この記事では WebGL 出力のスマホ動作のみに絞りました。スマホでゲームをプレイしてもらうにはサイズの最適化が肝要と思いますが、総合的な話でもあり、書き始めると膨大になりそうです。 参考 WebAssembly is here! https://blogs.unity3d.com/2018/08/15/webassembly-is-here/ Unity WebGL Loading Test https://github.com/JohannesDeml/UnityWebGL-LoadingTest
- 投稿日:2021-05-09T12:00:42+09:00
View Hierarchy ( adb dumpsys )
Androidアプリの不具合解析等で、View Hierarchyを見ることが良くあり、見方に困ったので纏めておきます。 View Hierarchyは名前の通り、Viewの階層を表します。 取得方法は下記 adb shell dumpsys activity top 取得される内容 View Hierarchy: DecorView@8b35a8a[MainActivity] android.widget.LinearLayout{aafc8fb V.E...... ........ 0,0-1080,2028} android.view.ViewStub{9373d18 G.E...... ......I. 0,0-0,0 #102018a android:id/action_mode_bar_stub} android.widget.FrameLayout{9b28371 V.E...... ........ 0,66-1080,2028} androidx.appcompat.widget.ActionBarOverlayLayout{f749556 V.E...... ........ 0,0-1080,1962 #7f080064 app:id/decor_content_parent} androidx.appcompat.widget.ContentFrameLayout{a7f9d7 V.E...... ........ 0,154-1080,1962 #1020002 android:id/content} androidx.constraintlayout.widget.ConstraintLayout{8ef12c4 V.E...... ........ 0,0-1080,1808} android.widget.ListView{d1ae5ad VFED.VC.. .F...... 0,0-1080,1808 #7f0800c4 app:id/sampleList} androidx.constraintlayout.widget.ConstraintLayout{73cb0e2 V.E...... ..S..... 0,0-1080,126} androidx.appcompat.widget.AppCompatTextView{3885c73 V.ED..... ..S..... 22,22-1058,104 #7f080080 app:id/function} androidx.constraintlayout.widget.ConstraintLayout{12c9730 V.E...... ........ 0,129-1080,255} androidx.appcompat.widget.AppCompatTextView{52d2fa9 V.ED..... ........ 22,22-1058,104 #7f080080 app:id/function} androidx.constraintlayout.widget.ConstraintLayout{e78f92e V.E...... ........ 0,258-1080,384} androidx.appcompat.widget.AppCompatTextView{d024ccf V.ED..... ........ 22,22-1058,104 #7f080080 app:id/function} androidx.constraintlayout.widget.ConstraintLayout{47bf65c V.E...... ........ 0,387-1080,513} androidx.appcompat.widget.AppCompatTextView{5001d65 V.ED..... ........ 22,22-1058,104 #7f080080 app:id/function} androidx.appcompat.widget.ActionBarContainer{b1f7a3a V.ED..... ........ 0,0-1080,154 #7f08002a app:id/action_bar_container} androidx.appcompat.widget.Toolbar{680e6eb V.E...... ........ 0,0-1080,154 #7f080028 app:id/action_bar} androidx.appcompat.widget.AppCompatTextView{1c48 V.ED..... ........ 44,40-651,114} androidx.appcompat.widget.ActionMenuView{5a2ae1 V.E...... ........ 1080,0-1080,154} androidx.appcompat.widget.ActionBarContextView{a7b0006 G.E...... ......I. 0,0-0,0 #7f080030 app:id/action_context_bar} android.view.View{b0506c7 V.ED..... ........ 0,2028-1080,2160 #1020030 android:id/navigationBarBackground} android.view.View{e1eb4f4 V.ED..... ........ 0,0-1080,66 #102002f android:id/statusBarBackground} 上記のView Hierarchyは、下記の画面を表示時に取得したものです。 View HierarchyからもListViewが表示されていることがわかります。 android.widget.ListView{d1ae5ad VFED.VC.. .F...... 0,0-1080,1808 #7f0800c4 app:id/sampleList} さらに表示されているViewの状態もわかります。 android.widget.ListView // クラス名 { d1ae5ad // HashCode V // Visibility (V, I, G) F // FOCUSABLE (F=true, .=false) E // ENABLED (E=true, .=false) D // draw (.=not draw, D=draw) . // SCROLLBARS_HORIZONTAL (H=true, .=false) V // SCROLLBARS_VERTICAL (V=true, .=false) C // CLICKABLE (C=true, .=false) . // LONG_CLICKABLE (L=true, .=false) . // CONTEXT_CLICKABLE (X=true, .=false) . // PFLAG_IS_ROOT_NAMESPACE (R=true, .=false) F // PFLAG_FOCUSED (F=true, .=false) . // PFLAG_SELECTED (S=true, .=false) . // PFLAG_PREPRESSED (p) / PFLAG_PRESSED (P=true, .=false) . // PFLAG_HOVERED (H=true, .=false) . // PFLAG_ACTIVATED (A=true, .=false) . // PFLAG_INVALIDATED (I=true, .=false) . // PFLAG_DIRTY_MASK (D=true, .=false) 0, 0 // mLeft, mTop - 1080, 1808 // mRight, mBottom #7f0800c4 // mID app:id/sampleList // PackageName } 参考 dumpsys : https://developer.android.com/studio/command-line/dumpsys?hl=ja View : https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/View.java;l=6440?q=view.java&ss=android%2Fplatform%2Fsuperproject&hl=ja
- 投稿日:2021-05-09T11:30:52+09:00
Maps SDK for Android でダークテーマ(のようなもの)
はじめに Maps SDK for Android でダークテーマにするにはどうすればいいのか。 オプションで darkMode みたいなものが無いのか 探したけど見つかりませんでした。 公式ドキュメントの中も、Qiitaの中も探したけれど見つからないのに、 探すのをやめたとき、見つかることもよくある話で、 スタイル設定 でできることに気づきました。 やりかた 公式ドキュメントを読んでね。 やりかた(ベータ版) 「Maps SDK for Android v.3.1.0 ベータ版」 では、「クラウドベースの地図のスタイル機能」が使えるようです。 アプリのアップデートよりも短期間に地図のスタイルを変える場合に有効ですが、そんなケースがあるんでしょうか・・・。ベータ版だし、あまり積極的に使いたくない感じです。 スタイル詳細 ダークモード相当のJSONスタイルは2つ見つけられました。 1つ目は、公式ドキュメントの中にあるサンプルのリソースです。 駅が真っ暗で「駅っぽさ」が無くなってしまいます。 2つ目は、公式ドキュメントからリンクがある Maps Platform Styling Wizard で作ったスタイルです。 駅はそのままですが、比較してみると「中央線」の文字が見やすくなっています。(個人の感想です) 一方、Google謹製のMapアプリを見てみましょう 圧倒的に見やすいですね! 駅もちゃんとしているし、謎に悪目立ちしてたオレンジの文字もいい感じになってます。 これを参考にJSONスタイルをぽちぽち作っていけば、オリジナルアプリでも同じ表示にできるんでしょうか。しかし、膨大なオプションが存在し、やってられません。 まとめ Maps SDK for Android に、darkMode みたいなオプションは無いが、スタイルを当てることでダークモードが可能 サンプルのダークモードスタイルはちょっと微妙 自力でスタイルJSONを作るのは非現実的 視認性の高いダークモードスタイルJSON、どっかにありませんか?