Androidでダークテーマ機能を実装する
はじめに
このようなFragmentの画面を用意しました.
実装方法
Step1 設定画面のレイアウトの表示
実はこれは普通のFragmentではなくてPreferenceScreenというコンポーネントを使って設定画面専用の画面みたいな感じで作成しています.
ざっくりと説明すると,いままで XMLをres/layout/fragment_preferences.xml
と作っていたものを res/xml/preferences.xml
みたいに作って中身をこんな感じにすると先程の画像のような画面になります.
<?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <PreferenceCategory android:key="categoryTheme" android:title="@string/theme_label"> <SwitchPreferenceCompat android:key="darkTheme" android:title="@string/dark_theme_label" /> </PreferenceCategory> <PreferenceCategory android:key="categoryLanguage" android:title="@string/language_label"> <ListPreference android:entries="@array/language_labels" android:key="language" android:title="@string/language_label" app:entryValues="@array/language_values" app:useSimpleSummaryProvider="true" /> </PreferenceCategory> </PreferenceScreen>
ちなみに,今回あまり関係ないですがあまり見慣れない @array
ですが,res/values/arrays.xml
のようなファイルを作ってその中に
<?xml version="1.0" encoding="utf-8"?> <resources> <array name="language_labels"> <item>日本語</item> <item>English</item> </array> <array name="language_values"> <item>JP</item> <item>EN</item> </array> </resources>
というような内容のものを作りました.
Fragmenのクラス内では PreferenceFragmentCompat()
というのを継承してあげて普段のFragmentの onCreateView
でしているように
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.preferences, rootKey) }
というのを書いてあげます.
Step2 ダークテーマ用のリソースファイルの作成
デフォルトである res/values
があると思いますが,res/values-night
というのを作ってstrings.xml
やcolors.xml
などを置いてあげるとダークテーマに切り替えたときのリソースファイルとして定義することができます.
今回は,res/values-night/themes.xml
を作りました.
<?xml version="1.0" encoding="utf-8"?> <resources> <style name="Theme.Todone.DayNight" parent="Theme.MaterialComponents.DayNight"> <item name="windowActionBar">false</item> <item name="windowNoTitle">true</item> <item name="colorPrimary">@android:color/black</item> <item name="colorPrimaryDark">@android:color/black</item> <item name="colorAccent">@android:color/black</item> </style> </resources>
ここで Theme.MaterialComponents.DayNight
っていうのがダークテーマにも対応したテーマを提供してくれてるやつなのでこれをparentに設定してあげます.
普通の res/values/themes.xml
にいるstyleのThemeのparentも同じようにして colorPrimary
などの色だけライトテーマとダークテーマ用に分けて書きます.
AndroidManifest.xml
のとこも忘れずに先ほどつくったThemeを当てるように書き換えます.
android:theme="@style/Theme.Todone.DayNight">
Step3 ダークテーマを端末の状態によって設定するようにする
Android 10からAndroid OSレベルでダークテーマを設定できるようになりました.
今回ではアプリを動かした端末がAndroid 10であった場合にその端末がダークテーマを設定した場合アプリもダークテーマにするやり方を紹介します.
こんな感じでApplicationを継承したAppクラスを作って以下のように実装します.
class App : Application() { override fun onCreate() { super.onCreate() setupNightMode() } private fun setupNightMode() { val nightMode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM } else { AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY } AppCompatDelegate.setDefaultNightMode(nightMode) } }
これで端末自体のダークテーマの設定に応じてアプリの方も自動でダークテーマに切り替わるようになります.
Step4 スイッチでダークテーマを切り替える
では実際にStep1で作ったレイアウト上にあるDark Themeスイッチを切り替えてダークモードとライトモードを切り替えていきます. 結果的にこのような感じで実装しました
class SettingsFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.preferences, rootKey) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) preferenceManager.findPreference<SwitchPreferenceCompat>(DARK_THEME_KEY).also { it?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if (newValue as Boolean) { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) } else { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) } return@OnPreferenceChangeListener true } } } companion object { private const val DARK_THEME_KEY = "darkTheme" } }
onPreferenceChangeListener
の中が実際にスイッチが切り替えた際に呼ばれます.
その中で, AppCompatDelegate.setDefaultNightMode()
を使って AppCompatDelegate.MODE_NIGHT_YES
を設定してあげることで自動的にダークモード(res/values-night/themes.xml)に切り替わります. AppCompatDelegate.MODE_NIGHT_NO
でライトテーマ(res/values/themes.xml)に設定します.
その他の詳細な知見
おそらく,一番簡単にダークテーマを実装するのは上記の方法だと思います.
最初,DroidKaigi2020のconference-app( https://github.com/DroidKaigi/conference-app-2020) を参考に実装していたのですが思わぬ挙動に遭遇して少し難しくて同じように実装できませんでした.その内容などをこのセクションで書きたいと思います.
今回ダークテーマを設定するFragmentではViewModelなどは使わずFragmentクラス内のみで実装をしました.
DroidKaigiの場合ざっくり説明するとViewModelを採用し,DarkThemeのスイッチが切り替えられたタイミングでViewModelに定義したメソッドを呼び出してそのメソッド内でViewModelのLiveDataを更新するようにしてその変更をFragmentでobserveして実際にAppCompatDelegate.setDefaultNightMode()
を動かしていました.
雰囲気で自分もそのように実装したのですが,スイッチを一回切り替えたときにViewModel側のLiveDataの値の更新も一度しかしていないのにobserverが2回呼ばれたような挙動になってテーマ切替時にAppCompatDelegate.setDefaultNightMode()
も2回呼ばれViewがカクカクする現象に陥りました.
これは,AppCompatDelegate.setDefaultNightMode()
が動いたときにActivityごと再生成される仕様であるのが原因でした.
1回目のobserverが動くのは純粋にスイッチを一回切り替えてLiveDataの値を更新したときの通知を受け取った分で2回目のobserverが動くのはAppCompatDelegate.setDefaultNightMode()
が動いてMainActivityが再生成された際に設定画面のFragmentも再生成され,その時にViewModelに置いているLiveDataが前回の値をFragmentが再生成されたときに値を通知している分であると考えられます.
これをDroidKaigiではきっといい感じにやっているんだと思いますが自分のレベルでは読み解けませんでした…
さいごに
現在学生を主体として7名で技術学習用にTODONEというTodoアプリをAndroidチーム,バックエンドチーム,デザイナーの3つのチームで開発中です. ここはもっとこういう感じに実装できるよなどアドバイスなどいただけたら嬉しいです. github.com
multi moduleで依存関係を記述するときのimplementationとapiの違い
DroidKaigi2020のconference-appをもとに説明します. 点線がimplementationを使った依存関係で実線がappを使った依存関係を表しています.
DroidKaigiのこの依存関係のグラフを見れば分かる通り,基本的にはimplementationを使って依存関係を記述していきます.これは例えば左下の:data:device
モジュールを見てみると,:model
モジュールと依存関係があります.この場合,:data:device
モジュールは:model
モジュールのことしか知りません.
他の点線で書かれている依存関係も一緒の意味です.:data:device
モジュールの右隣りの:data:db
モジュールは:data:api
モジュールと:model
モジュールを知っています.
では次に,実線で書かれている部分を見ていきましょう.:data:repository
モジュールと:model
モジュールは実線で繋がっている依存関係にあります.これが:data:repository
モジュールにapi project(':model')
とかって書いたときにできる依存関係です.
ここだけみると,implemantation
を使って依存関係を書いたときの変わりありません.
変わるのは:data:repository
モジュールとの依存関係を持つモジュールの挙動です.上のグラフでいうと,:android-base
モジュールの挙動です.
:android-base
モジュールは:data:repository
モジュールと点線で繋がっている依存関係のためimplementation
で繋がっています.なので,もちろん:android-base
モジュールは:data:repository
モジュールのことを知っています.しかし,その先の:data:repository
モジュールはapi
で繋がっているため,:android-base
モジュールは:model
のことも知ることができます.
api
を使うと依存関係に透過性ができるイメージです.
じゃあここで,もう一つ実線を使った依存関係が:android-base
モジュールと:feature:staff
モジュールにあることがわかります.しかし,ここでは:android-base
モジュールは誰からも知られておらず,api
を使った透過性のある依存関係は必要ないように感じます.
なぜ,ここでapi
を使った依存関係があるかというと,僕もあまり詳しいことは知らないのですが.feature:staff
モジュールがdynamic-feature-module
という特別なモジュールであるからです.ここではただこいつは特別なモジュールでこいつにはいつものapp
モジュール的な存在であるandroid-base
モジュールが必要ということだけわかれば良さそうです.
とにかく,api
を使った依存関係は透過性を持つということです.
現場からは以上です.
参考サイト
TextInputLayoutとTextInputEditTextのerrorとかの違いについての豆知識だよ
基本的に,TextInputLayout
の子にTextInputEditText
を入れて使う.
TextInputLayoutの設定
android:hint
を設定することでhintを設定することができる.これを設定するだけでEditTextをタップしたときにかっこいいアニメーション付きでhintが上に移動するやつが実装できる.hintの色はapp:hintTextColor
で設定できる.
よくある,ボタンを押したときに中身が入力されていないときのエラー表示については,TextInputLayoutにapp:errorEnabled="true"
を設定してあげればできるようになる.あとはKotlin側で書く.だいたい下の様な感じ.
button.setOnClickListener { var message: String when(TextUtils.isEmpty(edit_text.text)) { true -> { message = "入力されていません" text_input.error = message edit_text.error = message } false -> { message = "入力されています" text_input.error = null edit_text.error = null } }
ここで抑えておきたいのは親のTextInputLayoutとその子のTextInputEditTextの両方にerrorの処理を書けてそれぞれ挙動が違うこと.
TextInputLayoutに書くとこう.
TextInputEditTextに書くとこう.
両方組み合わせることもできる. ちなみに,helperっていうのを設定すればerrorと同じ位置にエラー時以外に下のラインの下にメッセージを表示させることができる.全体的にサンプルで作成したのは以下の感じ.
<com.google.android.material.textfield.TextInputLayout android:id="@+id/text_input" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="16dp" android:hint="This is hint" app:hintTextColor="@android:color/holo_blue_dark" app:errorEnabled="true" app:errorTextColor="@color/colorAccent" app:errorIconTint="@color/colorAccent" app:helperText="This is helper" app:helperTextTextColor="@android:color/black"> <com.google.android.material.textfield.TextInputEditText android:id="@+id/edit_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@android:color/holo_green_dark" android:maxLines="1"/> </com.google.android.material.textfield.TextInputLayout>
hintTextColor
とtextColotHint
の違いについて.
hintTextColor
はEditTextをタップしたときにシュッって上にいったhintの色のこと.textColorHint
はまだタップする前に表示されているhintの色のこと.
以下, app:hintTextColor="@android:color/holo_green_dark"
のとき
android:textColorHint="@android:color/holo_blue_dark"
のとき
Firebase Storageにあるファイルをダンロードする方法
すること
Step1 Google Cloud SDKをインストールする.
macOSの場合
ここのInstall the latest Cloud SDK versionからPackageをダウンロードして,Python2の環境がある場所で
$ ./google-cloud-sdk/install.sh
する.
次に
$ ./google-cloud-sdk/bin/gcloud init
Ubuntuの場合
基本的にここ通りにコマンドを実行していく.
$ echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
$ sudo apt-get install apt-transport-https ca-certificates gnupg
$ curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add -
$ sudo apt-get update && sudo apt-get install google-cloud-sdk
Step2 認証通す
$ gcloud init
を実行すると,このままログインしますか的なこと聞かれるので$ y
と答えると,ブラウザに飛んでログイン画面が出てきます.
ログイン後,ログインしたGoogleアカウントで作成しているcloud project(Firebaseで作成したProject)が一覧で出てくるのでダウンロードしたいStorageがあるプロジェクトを番号を入力して選択します.
Step3 bucket nameをコピー
Firebaseのconsoleを開いたときに左上ら辺にある gs://XXXXX.appspot.com
ってやつです.
Step4 コマンドでファイルをダウンロード
以下のコマンドでStorage内にある全ファイルをダウンロードします.
$ gsutil -m cp -R gs://<bucket_name> ./保存先
参考にしたサイト
Android始めて半年くらいの俺がContributeしかけたはなし
タイトルにある通り,Androidアプリ開発を去年の夏頃から初めて現在約半年くらいです.
そして今,2月に開催されるDroidKaigiに向けて全国のAndroidアプリエンジニアが集まってDroidKaigiのセッションリストなどをまとめたAndroidアプリをOSSで開発中です.
github.com
このリポジトリではIssueにwelcome contributeとタグが付けられているものは誰でも手を挙げて取り掛かることができます.
その中でも,easyとタグが付けられたIssueは僕のような初心者には取り掛かりやすいIssueとなっていて自分もなにかできないかなと思っていました.
そこで発見したこのIssue.
github.com
まだcloneしてソースコードも一度も見たことありませんでしたが,雰囲気自分でもできる感じがして手を挙げました.
OSSのコミットなんてしたことなかったので,すべてがわかりませんでした.
最後に,プルリク送るからどこかでbranch生やすんだよな…?
でもこれどっから生やすんだ…?
みんなどこでbranch切ってるんだ…?
調べました.
どうやらまず,forkする必要があるようです.
forkすることで自分のリモートに,コミットしようとしてる(今回で言うDroidKaigiのカンファレンスapp)リポジトリのコピーを作成します.cloneはローカルにコピーを作るのでそこが違う点かなと解釈しています.
forkすることにより,本家のリポジトリを汚さずにコミットすることができるようです.
本家のmasterから直接いろいろなcontributorがbranchを切ると,ごちゃごちゃになるのを防ぐイメージかなと解釈しています.
なので,まずは本家リポジトリをforkさせて自分のリモートリポジトリにコピーを用意し,それをcloneして開発します.
開発するときは,cloneしてきたリポジトリのmasterから任意の名前をつけてbranchを切って,開発が終わったら,そのbranchでプルリクを出すという流れです.
最初,ソースコードを見たとき,あまりこのような大きなプロジェクトファイルを読む機会がなかったので,自分が取りかかなければいけないソースファイルがどこにあるか探すのに少し苦労しました.
試行錯誤の末,想像よりも早くIssueを終わらせることができ,プルリクを出したのですが…
github.com
どうやら,僕よりも先に僕のIssueでなおす箇所を含む変更がコミットされていたみたいです.
残念ながら,僕のコミットが取り込まれることはなく,contributorになりかけたということです.
少し悲しいですが,僕が変更した部分も数行でこれでほんとにcontributorと名乗っていいのか疑問だったので,初めてOSSのIssueに取り掛かって,英語で会話してプルリクまで出せたという流れを体験できただけでもとてもいい経験だったかなと思っています.
プルリクもテンプレが用意されているのですが,スクリーンショットにgifを埋め込むなどやったことがないこともできて自分の中で知見が増えたのでそれも良かったです.
また,OSSにコミットする際のgitの扱いとして,本家リポジトリをforkするのですが,そのあと,コピーしたリポジトリでは本家の最新の状態をそのままでは追従できません.
そこで,gitのupstreamという機能を使う必要があるようで,それをすることによりコピーしたリポジトリでも本家のリポジトリの最新のmasterの状態を追従することができます.
僕も全然そこら辺詳しくないのですが,これを自分がプルリクを出す前にすることでコンフリクトを事前に回避できてトラブルが減るのではないかなと思っています.
ということで,ここまで語れるくらいには知見が溜まったので初心者の方でも恐れずにcontibuteしてみるといいと思います.思った以上の経験値が取得できます.
また機会があればなにかIssueやってみようかなと思っています.
人はこうして成長していくのである.
参考にした記事
GitHubでフォーク元の差分を取り込む - Qiita
開発用ブランチにMasterブランチの最新コードを取り込む - Beeeat’s log
2020やることリスト・やらないことリスト
2019年を振り返る
未来の自分のために今年がどんな年であったかを残しておきたいと思います.
いつかこれを自分が見たときに,懐かしさを感じたり新たな考え方を思いついたりできるかなと思うので.
今年は自分にとって大きな変化がたくさんあった年でした.
研究テーマを全く違うものに変えたり,モノを減らしてミニマリズムなライフスタイルを目指してみたり,継続的になにかしらブログとしてOUTPUTしようと思い立ってやってみたり,気になったインターンシップに申し込んで色々な日本を代表する企業にインターンシップに行ったり,いくつかの企業さんに内定をいただけたり.あと人生初めてのTOEIC受けたりしましたね.
2019年が始まった当初は全く思い描いていなかったことばかりを自ら行動して活動した非常に有意義で楽しい1年でした.
INPUTとして読んだ本は以下の8冊
- 1分で話せ 世界のトップが絶賛した大事なことだけシンプルに伝える技術
- ぼくらの未来をつくる仕事
- 教養としての社会保障
- 手ぶらで生きる。見栄と財布を捨てて、自由になる50の方法
- 時間術大全――人生が本当に変わる「87の時間ワザ」
- 「幸せ」について知っておきたい5つのこと NHK「幸福学」白熱教室 (中経出版)
- ぼくたちに、もうモノは必要ない。 - 断捨離からミニマリストへ -
- 学びを結果に変えるアウトプット大全
どれも自分の考え方とか価値観をアップデートしてくれた本ばかりです.本などを読んで著者など他の有能な人たちの考えを自分に取り入れて自分の人生に活かすのは個人的にとても面白いし楽しいので来年もぜひ続けたいと思っています.ミニマリスト関連で読みたい本も3冊くらいあるし,ビジネス書でも読みたくて読めていない本もたくさんあるので.
OUTPUTとして書いたブログ記事はこの記事を入れて31本.
ミニマリスト的なことを書いたりAndroidのことを書いたり色々書きました.今年書いた記事たちをさーっと読み返してみましたが,僕が想像していたようにやはり自分のそのときの考えなど文面など後で見返せる形で残すのはいいですね.あのときの自分はそんな事考えていたのかとか今の自分との距離を計れる気がします.
来年も癖にしていろんな記事を残していきたいです.
それに加えて,今年いくつかの企業さんから内定をいただけるまでに夏に思い切ってインターンシップに申込みをしたのは本当に良かったなと思っています.申し込んでもまあ落ちるだろうなあと思いながら申し込んでインターンに行けて結果その企業さんに内定を頂き,行くことにしました.
どうせ落ちるだろうからとか自分の中で自己完結させて終わらせるのではなく,とりあえずやってみることをしてみて本当に良かったと思っています.
今年のはじめには自分にとって好きな技術とか得意な技術分野というものがなかったのですが,今ではAndroidアプリ開発という好きなものができました.
来年は今年変化して色々やってみたものをどんどん成熟させて成長する年にしたいなと思います.
ミニマリズムなライフスタイルをもう少し洗練させて自分にとって必要,不必要なモノを厳選したり,Androidアプリ開発について知見を増やしたり,本格的にTOEICなど手を出した英語の学習に力を入れたりとできたらいいなと思っています.
皆様も良いお年を.