batchのブログ

知見の備忘録

Androidでダークテーマ機能を実装する

はじめに

このようなFragmentの画面を用意しました. f:id:batch08:20200407153519p:plain

実装方法

Step1 設定画面のレイアウトの表示

実はこれは普通のFragmentではなくてPreferenceScreenというコンポーネントを使って設定画面専用の画面みたいな感じで作成しています. ざっくりと説明すると,いままで XMLres/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.xmlcolors.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をもとに説明します. f:id:batch08:20200307193621p:plain 点線が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に書くとこう.
f:id:batch08:20200226204135p:plain

TextInputEditTextに書くとこう.
f:id:batch08:20200226204226p:plain

両方組み合わせることもできる. ちなみに,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>

hintTextColortextColotHintの違いについて.
hintTextColorはEditTextをタップしたときにシュッって上にいったhintの色のこと.textColorHintはまだタップする前に表示されているhintの色のこと.
以下, app:hintTextColor="@android:color/holo_green_dark" のとき
f:id:batch08:20200226204256p:plain android:textColorHint="@android:color/holo_blue_dark"のとき
f:id:batch08:20200226204328p:plain

Firebase Storageにあるファイルをダンロードする方法

f:id:batch08:20200207162827p:plain

すること

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やることリスト・やらないことリスト

新年あけましておめでとうございます.ミニマリストなエンジニアを目指しているbatchです.

今年のやることリスト・やらないことリストをサクッとまとめて書きたいと思います.
自分が定期的にこの記事を見返してぼーっと一年過ごさないように.

今年は学生生活最後の年なので,色々考えて無駄のないように過ごしたいと思っています.

まず,ミニマリストなのでミニマムに今年の目標的なものを掲げると,「今年終わったときに後悔しない」です.
一年後,今年あれやっておけばよかったなあなどと後悔しないようにしたいと思っています.

ということでまとめていきます.

やることリスト

  • 研究をある程度完成させて誰かに使ってもらえる状態までもっていく
  • Androidがんばる
  • Golangも凝った事はできなくともある程度のとこまでは開発できるレベルまで習得する
  • 海外進出する
  • 英語の勉強する
  • 好きなアーティストのライブはとりあえず行く
  • 読書する
  • 会計簿をつける
  • ブログ書く
  • 適度に運動する

やらないことリスト

  • 技術習得を目的としてAndroidGolang以外は手を出さない
  • なくても困らないようなモノの買い物をしない
  • 交際費をケチらない
  • 1日1食しない
  • 短時間睡眠しない
  • 他人に自分の考えを抑えつけない
  • 不健康な食事はしない
  • 衣服の買い物はしない
  • 恋人はつくらない
  • メインPCの買い替えない
  • 他人を軽蔑しない
  • すべてにYesと言わない
  • 自分じゃなくてもできることはやらない

以上,人生の中のたった1年ですが,時期的に今しかできないことも出てくる大事な1年だと思ってるので最善を尽くして後悔しないように過ごしたいと思います.

2019年を振り返る

未来の自分のために今年がどんな年であったかを残しておきたいと思います.
いつかこれを自分が見たときに,懐かしさを感じたり新たな考え方を思いついたりできるかなと思うので.

今年は自分にとって大きな変化がたくさんあった年でした.

研究テーマを全く違うものに変えたり,モノを減らしてミニマリズムなライフスタイルを目指してみたり,継続的になにかしらブログとしてOUTPUTしようと思い立ってやってみたり,気になったインターンシップに申し込んで色々な日本を代表する企業にインターンシップに行ったり,いくつかの企業さんに内定をいただけたり.あと人生初めてのTOEIC受けたりしましたね.

2019年が始まった当初は全く思い描いていなかったことばかりを自ら行動して活動した非常に有意義で楽しい1年でした.

INPUTとして読んだ本は以下の8冊

  • 1分で話せ 世界のトップが絶賛した大事なことだけシンプルに伝える技術
  • ぼくらの未来をつくる仕事
  • 手ぶらで生きる。見栄と財布を捨てて、自由になる50の方法
  • 時間術大全――人生が本当に変わる「87の時間ワザ」
  • 「幸せ」について知っておきたい5つのこと NHK「幸福学」白熱教室 (中経出版)
  • ぼくたちに、もうモノは必要ない。 - 断捨離からミニマリストへ -
  • 学びを結果に変えるアウトプット大全

どれも自分の考え方とか価値観をアップデートしてくれた本ばかりです.本などを読んで著者など他の有能な人たちの考えを自分に取り入れて自分の人生に活かすのは個人的にとても面白いし楽しいので来年もぜひ続けたいと思っています.ミニマリスト関連で読みたい本も3冊くらいあるし,ビジネス書でも読みたくて読めていない本もたくさんあるので.

OUTPUTとして書いたブログ記事はこの記事を入れて31本.
ミニマリスト的なことを書いたりAndroidのことを書いたり色々書きました.今年書いた記事たちをさーっと読み返してみましたが,僕が想像していたようにやはり自分のそのときの考えなど文面など後で見返せる形で残すのはいいですね.あのときの自分はそんな事考えていたのかとか今の自分との距離を計れる気がします.
来年も癖にしていろんな記事を残していきたいです.

それに加えて,今年いくつかの企業さんから内定をいただけるまでに夏に思い切ってインターンシップに申込みをしたのは本当に良かったなと思っています.申し込んでもまあ落ちるだろうなあと思いながら申し込んでインターンに行けて結果その企業さんに内定を頂き,行くことにしました.
どうせ落ちるだろうからとか自分の中で自己完結させて終わらせるのではなく,とりあえずやってみることをしてみて本当に良かったと思っています.

今年のはじめには自分にとって好きな技術とか得意な技術分野というものがなかったのですが,今ではAndroidアプリ開発という好きなものができました.

来年は今年変化して色々やってみたものをどんどん成熟させて成長する年にしたいなと思います.
ミニマリズムなライフスタイルをもう少し洗練させて自分にとって必要,不必要なモノを厳選したり,Androidアプリ開発について知見を増やしたり,本格的にTOEICなど手を出した英語の学習に力を入れたりとできたらいいなと思っています.

皆様も良いお年を.