Diary

Diary

日々学んだことをアウトプットする場として初めてみました

Android のモジュールで BuildConfig を生成しない方法

この間マルチモジュールの対応をしたのですが、その際に BuildConfig について少し気になったので、今回はモジュールにおいて BuildConfig を生成しない方法についてメモしておきます。

各モジュールにおいても、標準では以下のようなファイルが generated フォルダに生成されます。
build/generated/source/buildConfig/ の中にあります。)

package jp.mydns.kokoichi0206.common;

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String LIBRARY_PACKAGE_NAME = "jp.mydns.kokoichi0206.common";
  public static final String BUILD_TYPE = "debug";
}

これをモジュールで生成しないようにするには、module の build.gradle に以下のように設定します。

plugins {
    ...
}

android {
    ...
    libraryVariants.all {
        it.generateBuildConfig.enabled = false
    }
}
...

マルチモジュールでの compose navigation におけるベストプラクティス(Android DevSummit)

Compose の nagitaion における multi module 対応についての内容を youtube で見ました("Type safe, multi-module best practices with Navigation Compose")。

この中で 5 つのベストプラクティスが紹介されていたため、簡単に紹介します。

Compose のスクリーンでは State を入力とし events を出力とせよ

@Composable
fun ColumnWithLine(
    uiState: MemberListUiState,
    onNavigateToParticipantList: (conversationId: String) -> Unit,
) {
    ...
}

Screen はデータがどこから来たかを気にするべきではなく、例えば viewmodel であったり、適当な値が入ってようが動くようにするべきである。
また、誰がイベントを受け取るかも気にするべきではない。

独立性を高めて、テストを容易にするメリットもある。

画面ごとに feature module を分解した際に、app module 等で Navigation の設定をするかと思います。
『その Navigation のグラフ設定も、feature module 側に含めよ』ということです。

ルートの設定をするのは、使われる側(モジュール)の役目ということですね。

feature/settings/navigation/settings.kt

const val settingsRoute = "settings_route"

// NavGraphBuilder にメソッドを生やす
fun NavGraphBuilder.settingsScreen(
    onThemeChanged: (String) -> Unit,
) {
    composable(route = settingsRoute) {
        SettingsScreen {
            onThemeChanged(it)
        }
    }
}

例えば app 側で呼び出すには以下のようにします。

@Composable
fun BottomNavHost(
    navHostController: NavHostController,
    onThemeChanged: (String) -> Unit
) {
    NavHost(
        navController = navHostController,
        startDestination = BottomNavItem.Home.route
    ) {
        homeScreen()

        // こんな感じで使える!
        settingsScreen(onThemeChanged)

必要な Public API のみを公開せよ

Argument の設定等、公開しなくてもいいものは internal をつけるなどして、他モジュールに公開しないようにします。

また、VisibleForTesting annotation を使ってテストように公開することも可能です。

@VisibleForTesting
internal const val authorIdArg = "authorId"

internal class AuthorArgs(val authorId: String) {
    constructor(savedStateHandle: SavedStateHandle, stringDecoder: StringDecoder) :
        this(stringDecoder.decodeString(checkNotNull(savedStateHandle[authorIdArg])))
}

Module 構造と Graph 構造はセットに考えるべき

module を分割しその公開 API を決めることで、Graph 構造も定まるようにするべきです。

特に、module 間で遷移することはやめるべきであり、遷移メソッドを公開し上位メソッドから呼び出すように修正します。

公開する遷移のためのメソッドを NavController に生やす。

fun NavController.navigateToMemberDetail(member: Member) {
    this.navigateUp()
    this.navigate(
        memberDetailRoute
                + "/$memberJson=${getJsonFromMember(member)}"
    )
}

上位モジュールから呼び出す。

NavHost(
    navController = navHostController,
    startDestination = BottomNavItem.Home.route
) {
    memberListScreen {
        navHostController.navigateToMemberDetail(it)
    }

リソースを随時確認せよ

最後のベストプラクティスは、以下リソースを随時確認しよう、ということです。

おわりに

NowInAndroid app: sample app のアプリは非常に完成度高そうなので、積極的に参考にしていきたいです。

Jetpack compose で Back-end (JVM) Internal error

Jetpack compose で開発中、以下のようなエラーが出ました。

Caused by: org.jetbrains.kotlin.codegen.CompilationException: 
    Back-end (JVM) Internal error: Couldn't inline method call: 
    CALL 'public final fun Column (modifier: androidx.compose.ui.Modifier, 
    verticalArrangement: androidx.compose.foundation.layout.Arrangement.Vertical, 
    horizontalAlignment: androidx.compose.ui.Alignment.Horizontal, content: 
    @[Composable] @[ExtensionFunctionType] kotlin.Function1<androidx.compose.foundation.layout.ColumnScope, kotlin.Unit>):
    kotlin.Unit [inline] declared in androidx.compose.foundation.layout.ColumnKt' type=kotlin.Unit origin=null
Method: null
File is unknown
The root cause java.lang.IllegalStateException was thrown at: 
org.jetbrains.kotlin.codegen.inline.SourceCompilerForInlineKt.getMethodNode(SourceCompilerForInline.kt:118)
    at org.jetbrains.kotlin.codegen.inline.InlineCodegen.performInline(InlineCodegen.kt:63)
    at org.jetbrains.kotlin.backend.jvm.codegen.IrInlineCodegen.genInlineCall(IrInlineCodegen.kt:163)
    ...

エラーメッセージから何のことか自分には特定に時間がかかったため、こちらにメモしておきます。

原因

buildFeatures に compose が設定されてない。

途中から compose に切り替えた場合や Module を作成した場合ではこちらが入ってないので、build.gradle に入れてやる必要があります。

plugins {
    ...
}

android {
    ...
    // 追加
    buildFeatures {
        compose true
    }
    ...
}
...

bash スクリプト初心者が知っておきたいことまとめ

いまだに自分も初心者ですが、 初心者から見て初めに知っておきたかったこと思いがけず詰まったことをメモしておきたいと思います。
誰か(来年のぼく)の参考になればと思います。

[目次]

環境

- Machine: Raspberry Pi 4 Model B Rev 1.4
- OS: Linux ubuntu 5.4.0-1045-raspi
- Bash version: GNU bash 5.0.17(1) aarch64

結論

* ShellCheck を導入しよう
    * VSCode の拡張もあるよ
* まとめられない

スタイル編

この辺は ShellCheck で圧倒的にカバー可能。

変数

変数定義では前後に余計なスペースは絶対入れてはなりません

a="hoge"
b=pien
# c: command not found というエラーになる。
c = huga

echo "$a" "$b" "$c"

[ はコマンドです!

[ は例えば次のように使います。

$ cat test.sh
#!/bin/bash

a=2022
if [ $a -gt 2000 ]; then
    echo "2000年代です。"
fi

# これはアウト
# if [$a -gt 2000]; then

この時、[ の前後に変なスペースを入れてはいけません
これは [ がコマンドの一部であるためであるためです。

コマンドの後は引数やオプションが来ると思うのですが、その時にはスペースを開けると思います、その感覚です。
コマンドであること・使い方は、実際に man で確認できます。

$ man [
NAME
       test - check file types and compare values

SYNOPSIS
       test EXPRESSION
       test
       [ EXPRESSION ]
       [ ]
       [ OPTION
...

エラーが起きてもスクリプトは終わらない

$ cat test.sh
#!/bin/bash

a="hoge"
# Error!!
c = huga

echo "$a" "$b" "$c"

上記スクリプトc = huga の部分でエラーになるので、そこで通常はプログラムの実行が止まるのですが(少なくともぼくにとっては)、実はエラーが起きても止まりません!
最後まで強制的に実行を続けます(実はこれは bash の起動時のオプションに依存)

# 上記ファイル実行時の出力
$ bash test.sh
test.sh: line 5: c: command not found
hoge

エラー時にスクリプトを止めるオプション

errexit のオプションを on にすることで、エラー時に終了させることが可能です。

# デフォルトでは off
$ set -o | grep errexit
errexit         off

修正したスクリプト

$ cat test.sh
#!/bin/bash
set -e

a="hoge"
# Error!!
c = huga

echo "$a" "$b" "$c"

実行

# c の定義以降の echo が呼ばれてないことが分かる。
$ bash test.sh
test.sh: line 6: c: command not found

未定義変数の取り扱いに注意する

通常では、未定義変数を参照した時もスクリプトは止まりません!

このことは、時に恐ろしい事態を招いてしまいます。

#!/bin/bash

data_dir="/home/ubuntu/Documents/work/pien/data"
# path_to_file が定義されてない時(実装ミスがあった時)
# フォルダ全部が削除されてしまう。
rm -r "${data_dir}/${path_to_file}"

未定義変数使用時にスクリプトを終了するオプション

エラー時同様、bash 起動時のオプションをつけてあげることで回避可能です。

# デフォルトでは off
$ set -o | grep nounset
nounset         off

修正したスクリプト

$ cat test.sh
#!/bin/bash
set -u

data_dir="/home/ubuntu/Documents/work/pien/data"
rm -r "${data_dir}/${path_to_file}"
echo "ここまで来るかな?"

実行

# 未定義変数参照時にスクリプト終了となる。
# 『ここまで来るかな』は来ない!
$ bash test.sh
test.sh: line 5: result: unbound variable

パイプは最後の終了ステータスしか見ない

bash においては true, false もコマンド(see: man true(false))なので、それを使って確かめてみます。

$ cat test.sh
#!/bin/bash

exit 128 | exit 64 | exit 0
echo $?

# パイプのうち、最後のコマンドの終了ステータスが反映される。
$ bash test.sh
0

これは困ります。
パイプ全体の終了ステータスは、PIPESTATUS で取得可能です。

$ cat test.sh
#!/bin/bash

exit 128 | exit 64 | exit 0
echo "${PIPESTATUS[@]}"

$ bash test.sh
128 64 0

パイプ失敗時にエラーを吐かせる

こちらも、pipefail のオプションで指定可能です。

$ cat test.sh
#!/bin/bash
set -o pipefail

# 3 が全体の終了コード、右から組み立てられる感じ?
exit 128 | exit 64 | exit 3 | exit 0 | exit 0
echo $?
echo hoge

$ bash test.sh
3
hoge

パイプがこけた時点でスクリプトを終了するには、-e と組み合わせて set -eo pipefail と指定しておけば問題ないです。

パイプはそれでも実行されます

set -o pipefail を指定することで、パイプの一部でもこけたらエラーを吐かせるようにすることができました。

ただ、パイプは『繋げて前の出力を待つ』という性質上、組み立てた時点で実行されます!
(パイプ元がパイプ先に接続する時、パイプ先が待機可能になっている必要があるイメージ)

$ cat test.sh
#!/bin/bash
set -eo pipefail

true | false | echo hoge

$ bash test.sh
hoge

引数と標準入力を区別する

bash に慣れてきてパイプを繋げまくっていた頃、xargs が何のためにあるのか一瞬迷子になりました。
それはぼくが引数と標準入力を意識してなかったことに起因してます。

パイプは標準出力と標準入力を繋げて遊ぶゲームです。
そのため基本的にパイプの駒の候補となるのは、標準出力を受け付けるコマンドです。
(引数と標準入力をどちらも取るコマンドも多く存在します。)

そこで引数を受け付けたい場合に、xargs を使う、という戦法になります。

$ man xargs
NAME
       xargs - build and execute command lines from standard input
...

一番簡単な例を示します。

# echo は引数しか受け付けないため、このワンライナーは何も出力されない。
$ echo hoge | echo

# xargs を用いてコマンドを使う。
$ echo hoge | xargs echo
hoge

xargs は外部コマンドのみを対象とする!

ビルドインコマンドと外部コマンドについても意識する必要があります。
挙動が異なるものも多く、ここでは馴染み深い echo を題材に取り上げます。

とりあえず echo について確認します。

# echo の挙動
$ type echo
echo is a shell builtin
$ type -a echo
echo is a shell builtin
echo is /usr/bin/echo
echo is /bin/echo

## ビルドインコマンド(通常)
$ echo -e '\uFF10'## 外部コマンド
$ /usr/bin/echo -e '\uFF10'
\uFF10

xargs では『外部コマンド』が呼ばれていることを確認します。

$ echo -e 'FF10' | xargs -I@ echo -e '\u@'
\uFF10
$ echo -e 'FF10' | xargs -I@ which echo
/usr/bin/echo

ではビルドインコマンドを使いたい場合はどうしたらいいのか。
→ シェルを明示的に呼んであげるとよさそうです。

$ echo 'FF10' | xargs -I@ bash -c 'echo -e "\u@"'

シェルスクリプト実行中に適時シェルスクリプトを読み込む

京大のスパコンのデータが吹っ飛んだ原因です

シェルスクリプトは呼び出されるまで呼び出されません!
(うい)

以下の2ファイルを使って簡単に確認してみます。

test.sh

#!/bin/bash

echo "script start"

sleep 15

echo "test"  # ①
# サブファイル(echo_script.sh)を実行する
bash ./echo_script.sh

echo_script.sh

#!/bin/bash

echo "Echo from another script file"  # ②

sleep 15 の間に ①, ② をそれぞれ変えてどうなるかを確認してみます。

  • ①: 元の文言が出力される
  • ②: 新しい文言が出力される!

どうやらサブファイルの読み込みなどにおいては、実行時に初めて読み込まれるようです。

これは python 等他のスクリプト言語とは異なるため、時間のかかるスクリプトで複数ファイルに分割している際は注意が必要でしょう。

リダイレクトはファイルを初期化する

$ cat hoge
some text

# こういうのは良くない。
# > の時点で対象のファイルが初期化されてしまう。
$ cat hoge > hoge
# hoge は空っぽになってしまっている。
$ cat hoge

コマンドによっては終了ステータスが 0 じゃない

終了ステータスが 0 より大きいものは異常(エラー?)と習うかと思います。

しかし、コマンドによっては(ぼくにとって)予想外のタイミングで 1 を返すことがあります。
errexit が有効になっている bash 環境などでは、こちらが変に効いてきてしまうので注意が必要です。

find 系が多いのかなーと思うのですが、した 2 つは実際に困ったことのあるコマンドです。

diff

diff は差分がなかった時が 0、差分があった時が 1、ファイルがなかった時などのエラーが 2 で終了します。

$ diff <(echo hoge) <(echo pien)
...
$ echo $?
1

grep

grep も diff と同様です。
(差分がなかった時が 0、差分があった時が 1、ファイルがなかった時などのエラーが 2)

# no grep result
$ grep bine test.sh
$ echo $?
1

# no such file
$ grep bine test.she
grep: test.she: No such file or directory
$ echo $?
2

終わりに

随時更新していきます。

emulator-5554 offline の退治法

emulator-5554 offline の退治法(Linux, macOS

emulator は 1 つも立ち上げてないはずなのに、more than one device/emulator と表示され adb コマンドや scrcpy などが動作しない場合があります。

adb shell dumpsys package d
adb: more than one device/emulator

この時、他の端末・エミュレーターとかないのにな〜〜とか思いながら adb devices を叩くと、全く意図しない emulator-5554 が存在するよ〜と言われました。

$ adb devices
List of devices attached
adb-8BSX1EC56-SlLKka._adb-tls-connect._tcp.     device
emulator-5554   offline

今回はこの emulator-5554 offline の退治法についてまとめます(Linux, macOS)。
(もちろん adb コマンドのオプションを利用し、端末を指定して起動しても問題ないですが、無駄なプロセスがいるのも気持ち悪いので対応しましょう。)

削除法

5554 は何となく想像つくかもしれませんが、ポート番号だと思われます。

そこで lsof(list open files) コマンドを利用し、指定した port で何のアプリケーションが動いてるかを特定します。
Linux, macOS

$ lsof -i

# 特定のポートに絞って表示する
$ lsof -i:5554
COMMAND     PID     USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
qemu-syst 52205 kokoichi   48u  IPv4 0x19d43a316fc9e1b3      0t0  TCP localhost:sgi-esphttp (LISTEN)
qemu-syst 52205 kokoichi   49u  IPv6 0x19d43a3166b04b03      0t0  TCP localhost:sgi-esphttp (LISTEN)

問題なさそうなら、PID を指定して抹殺します。

kill -KILL 52205

結果として、無事意図した(自分で把握してる)1台のみになりました!

$ adb devices
List of devices attached
adb-8BSX1EC56-SlLKka._adb-tls-connect._tcp.     device
emulator-5554   offline

adb コマンドで端末を指定して起動する方法

adb コマンドで端末を指定して起動する方法

複数端末・エミュレータが接続されているケースにおいて、adb コマンドを使う際に特定の端末を指定する方法です。

# シリアル番号を特定する
$ adb devices
List of devices attached
1C221FDF600AW6  device  # 1C221FDF600AW6 の部分
adb-8BSX1EC56-SlLKka._adb-tls-connect._tcp. device

# -s でシリアル番号を指定
adb -s 1C221FDF600AW6 shell dumpsys package d
adb -s 1C221FDF600AW6 shell pm list packages

# USB デバッグで繋がってる端末が1つのみの場合
adb -d shell ...

help をみた感じ、-d, -e, -s, -t あたりが使えそうかと思いました。

$ adb --help | grep device
 -d         use USB device (error if multiple devices connected)
 -e         use TCP/IP device (error if multiple TCP/IP devices available)
 -s SERIAL  use device with given serial (overrides $ANDROID_SERIAL)
 -t ID      use device with given transport id
...

おまけ: scrcpy

scrcpy も同様に指定できます。

scrcpy -s adb-8BSX1EC56-SlLKka._adb-tls-connect._tcp.

GitHub Actions 個人用チートシート

GitHub Actions ハロワの記事も書いているので、よかったらご覧ください。

基本的にはドキュメントがしっかりしているのでそちらで足りるかと思うのですが、個人的に何度も調べてるものはこちらにまとめておきます。

[目次]

注: 以下は 2022/10/22 現在の情報となります。
うまくいかない場合は、公式のドキュメントをご覧ください。

トリガー

何を契機に jobs を走らせるかを指定します。
Events that trigger workflows

手動実行できるようにする

onworkflow_dispatch: を追加する。

on:
  # 手動実行できるようにする!
  workflow_dispatch:

定期実行

スケジュール実行(定期実行)を行う方法について。
(schedule について)

onschedule: を追加する。
デフォルトでは UTC なので注意(日本は UTC+9)。

また、正確な時間ではないのでその辺許してあげてください。
(そのため、12 時ぴったりに処理が走らないと困る、という場合には適してないです。)

on:
  schedule:
    # 日本時間23時00分ごろの指定
    # 毎日 23 時 17-20 分ごろに
    - cron: "0 14 * * *"

特定のパス/拡張子の時は実行させない

マッチパターンの意味

on:
  pull_request:
    paths-ignore:
      # docs フォルダ配下の、全ファイル。
      - "docs/**"
      # 全ディレクトリの md ファイル。
      - "**.md"

複数条件実行

基本的には matrix をうまく使います。

複数バージョンで走らせる

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.16", "1.18"]
    steps:
      - name: Setup go
        uses: actions/setup-go@v3
        with:
          go-version: ${{ matrix.go }}
      - name: checkout
        uses: actions/checkout@v3
      - name: Testing
        run: go test ./...

複数マシン(OS)で走らせる

runs-on に指定します

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os:
          - ubuntu-latest
          - macOS-latest
          - windows-latest
    steps: ...

CI ファイルを分割する

jobs 部分が長くなってくると、各ステップでやってることが把握しにくくなり、保守性が下がってくるという問題が発生します。

そういう時は、ファイルを分割しメインファイルから呼んであげることができます。
また、他リポジトリにあるファイルも呼び出せるため、使い回しが可能になります。

reusable-workflows を使います。

reusable 側(呼び出される側)

呼び出し条件 (on:) を workflow_call で定義します。

name: Local test

on:
  workflow_call:
    secrets:
      ENCODED_RELEASE_KEYSTORE:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-20.04
    timeout-minutes: 10

    environment: production
    steps:
      - ...

使う側(呼び出す側)

marketplace から job を使うように uses で使用できます。

jobs:
  call-workflow-passing-data:
    uses: octo-org/example-repo/.github/workflows/reusable-workflow.yml@main
    with:
      username: mona
    secrets:
      envPAT: ${{ secrets.envPAT }}

そのほか

job の実行順序を制御

基本的には job は並列で進んでいくのですが、『リントやビルドが通った場合のみ単体テストを走らせたい』など、順番を調整したい場合があります。

そんな時は need を使います。

jobs:
  build:
    uses: kokoichi206/xxx

  lint:
    uses: kokoichi206/yyy

  local-test:
    # この job を走らせるには、build, lint の終了が必要
    needs: [build, lint]
    uses: kokoichi206/zzz

  android-emulator-test:
    # この job を走らせるには、build, lint の終了が必要
    needs: [version-check, build, lint]
    uses: kokoichi206/xyz

step 間で値を共有する [非推奨]

: set-output非推奨になったようです!!

jobs:
  checker:
    runs-on: ubuntu-20.04
    steps:
      - name: Get version of BASE_REF
        id: id-version
        run: |
          echo "::set-output name=version::1.0.0"
      - name: Get version of HEAD_REF
        run: |
          base="${{ steps.id-version.outputs.version }}"
          echo "${base}"

step や job の間で値を共有する

上の方法は 2022/10/18 に非推奨になっているので、今後は『Passing values between steps and jobs in a workflow』に従って値を共有することになります。

step 間
実行例

steps:
  - name: Set the value
    run: |
      echo "test_value=pien" >> $GITHUB_ENV

  - name: Use the value
    run: |
      # This will output 'pien'
      echo "${{ env.test_value }}"

job 間
実行例

jobs:
  # outputs で共有
  Between-jobs-1:
    runs-on: ubuntu-latest
    # Map a step output to a job output
    outputs:
      output1: ${{ steps.step1.outputs.test_value }}
    steps:
      - id: step1
        run: |
          echo "test_value=hello" >> $GITHUB_OUTPUT
  Between-jobs-2:
    runs-on: ubuntu-latest
    needs: job1
    steps:
      - run: echo ${{needs.Between-jobs-1.outputs.output1}}

おわりに

随時便利そうなものがあれば追加いたします。