Clean Architecture, Clean Life

仕事・個人での技術的なことつぶやきます

【Android】WorkerFactoryをDaggerでDIする

経緯

プロダクトで定期的に実行する機能の開発があり、Workerというものを採用することになりました

その際にいくつか詰まったところがあったので、使い方や使用感をまとめたいと思います

Workerとは

困ったところ

WorkerはWorkManagerというAndroidフレームワークによって生成されます

その際にコンストラクタで受け取れる引数はContextとWorkerParametersのみで、他のオブジェクトを受け取ることはできません

それを実現するためには、独自のWorkerFactoryを作り、WorkManagerに渡してあげる必要があります

手順

  1. Workerを作成する

今回は独自のLogユースケースクラスを引数に取るWorkerを作ります

class LogWorker(
    context: Context,
    params: WorkerParameters,
    private val logUseCase: LogUseCase
) : Worker(context, params) {
    override fun doWork(): Result {
        logUseCase.start()
        return Result.success()
    }
}
  1. WorkerFactoryを継承したLogFactoryを作成
class LogWorkerFactory(
    private val logUseCase: LogUseCase
) : WorkerFactory() {
    override fun createWorker(
        appContext: Context,
        workerClassName: String,
        workerParameters: WorkerParameters
    ): ListenableWorker? {
        return if (workerClassName == LogWorker::class.java.name){
            LogWorker(appContext, workerParameters, logUseCase)
        } else {
            null
        }
    }
}
  1. 作成したLogWorkerFactoryを呼び出したいクラスからWorkManager.initialize()メソッド経由でWorkerManagerへ登録
class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        WorkManager.initialize(
            this,
            Configuration.Builder().setWorkerFactory(LogWorkerFactory())
                .build()
        )
    }

}
  1. デフォルトのWorkManagerInitializerを削除する旨をマニフェストファイルに記載
<manifest ...>    
    <application  
        ...>  
  
        ...  
  
        <provider  
            android:name="androidx.work.impl.WorkManagerInitializer"  
            android:authorities="${applicationId}.workmanager-init"  
            tools:ignore="ExportedContentProvider"  
            tools:node="remove" />  
  
    </application>  
</manifest>  
  1. 通常通り呼び出しを行う()

今回は定期実行ということでPeriodicWorkRequestクラスを利用します

qiita.com

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val workManager = WorkManager.getInstance()
        val logPeriodicWorkRequest = PeriodicWorkRequest.Builder(
                    LogWorker::class.java,
                    Duration.ofMinutes(15)
                ).build()
                val operation = workerManager.enqueue(logPeriodicWorkRequest)
        workManager.enqueue(OneTimeWorkRequest.from(LogWorker::class.java))
    }
}

通常はここで終わりなんですが、プロダクトではDaggerを使っているので、provide メソッドによる管理を行いたいと思います

WorkerをDIしようとすると結構な手順が必要になるので、今回はWorkerではなく、WorkerFactoryのみDIしたいと思います

WorkerごとDIしたい場合は下記を参照

akihito104.hatenablog.com

qiita.com

tech.studyplus.co.jp

手順は簡単です

  1. provideメソッドをモジュールにセット
    @Provides
    @Singleton
    fun provideWorkerFactory(
        logUseCase: LogUseCase
    ): WorkerFactory {
        return LogFactory(logUseCase)
    }
  1. 呼び出しもとでInjectする
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var workerFactory: WorkerFactory

    override fun onCreate(savedInstanceState: Bundle?) {
        (省略)
    }
}

まとめ

今回はDagger環境でWorkerを使う方法について解説しました

定期実行したいという際にまず出てくるのがWorkerという選択肢だと思うので、今後も使う機会があるのではないでしょうか

今回参考にさせていただいたページは下記です

今回も最後まで読んでいただいてありがとうございました

y-anz-m.blogspot.com

www.webdevqa.jp.net

https://proandroiddev.com/dagger-2-setup-with-workmanager-a-complete-step-by-step-guild-bb9f474bde37

【storybook】使ってみたけど、フロントエンジニアではない私にとっては使いにくかった

経緯

技術調査の一環で、storybookというwebアプリのデザインカタログを作れるライブラリがあるとのことで使ってみた

storybookとは

上にも書いたとおり、webアプリのデザインカタログを作れるライブラリで、HTMLやJSはもちろん、Vue・React・AngularなどのSPAたちにも対応している

デザイナーとのやり取りや、Componentの重複を防ぐことを目的として作られることが多いようだ

結論

それに適したアーキテクチャのアプリでかつ、慣れないと少し使いづらい

使っている人の間ではかなり絶賛されているので、使える人からすれば素晴らしい機能なんだと思うんですが、

そもそもフロントエンドエンジニアではない私にとってはちょっと使いこなすのが難しかったです

もう少し詳しく説明していきたいと思います

使いにくいポイント① 最新版(v6)のドキュメントが少ない

私の調べた限りだとこのくらい

qiita.com

mokajima.com

この記事を書いている時点での最新がv6系なのですが、出回っている記事のほとんどが古いバージョンに関しての記事

v5→v6でファイル構成だったり、アドオンの種類だったり、コマンドだったり結構変わっているので、使ったことある人じゃなければv5以前の記事を見ながらv6を使っていくのは至難の業だと思います

しかしv6になってから結構立つのになんで記事が少ないんでしょうか?

古いバージョンのまま使っている人がほとんどなのか、もしくはもう誰も使っていないのか

使いにくいポイント② エラーが出ない

私の場合は別のプロジェクトですでにできているアプリを使ってstoryを書いてたんですが、なぜかstoryを読み取ってくれない

エラーとかExceptionとか出してくれたらいいのにそれもない

だから何がだめなのかわからない

しかもドキュメントもない・・・\(^o^)/オワタ

この記事が出るたびにイライラしていました笑

使いにくいポイント③ 既存のアプリケーションに導入する場合はアプリを選ぶ

上記も書いた通り、アトミックデザインのような再利用を前提としたデザイン・Component構成になっていない限りあまり使う旨味はないと思います

使うためには既存のソースコード+storyを記載するファイルをComponent単位で作る必要があります

これだけでかなり工数が増えるのに加え、storyファイルには依存関係も全て記載していかなければならず、もとのComponentの依存関係が変わったらStoryも書き換えなければいけないため、開発だけではなく、メンテナンスの工数もかかってしまいます

ただ、きっちり再利用できるComponentごとに分割して開発できているプロジェクトってそんなにないんじゃないでしょうか?(偏見)

言い換えてみれば、今から作るプロジェクトに関してはこれを導入することで、強制的にアトミックデザインに持っていくようなガード的な役割もできるかもしれないですね

最後に

今回は私の専門外の話なので少し短いですが終わりたいと思います

有識者の方で、もし私の記事の中で間違っている所があればご指摘いただけると幸いです

今回も最後まで読んでいただきありがとうございました

それでは失礼致します

【GithubActions】Androidアプリの署名設定

経緯

Androidアプリのビルド時、現状はビルド後に別で署名処理を走らせているのですが、ビルドと署名を同時に行うよう設定したいと思って調べていました

いくつか方法は出てきたのですが、何回やってもkeytoolで「署名付きJARファイルではありません」という状態でうまくいきませんでした

一応解決したので備忘録として記録しておきます

方法

結論としては、「v1Signを有効にする必要があった」ということです

見てもらったほうが早いと思うのでいかにサンプルを記載します

サンプル

まずはbuild.gradle(appの方)にキー情報を記載します

android {
    ...
    signingConfigs{
        release{
            storeFile rootProject.file('release.keystore')
            storePassword System.getenv('KEYSTORE_PASSWORD')
            keyAlias System.getenv('KEY_ALIAS')
            keyPassword System.getenv('KEY_PASSWORD')
            v1SigningEnabled true
            v2SigningEnabled true
        }
    }
    
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
        ...
   }
...
}

ここで「v1SigningEnabled trueと「v2SigningEnabled true」を忘れないでください

そしてリリースビルド時に署名を行いたい場合は、BuildTypeにその旨を記載します

また、個々に記載した署名鍵の情報はGithubのシークレットに記載する必要があります

手順としては、Githubの「Settings」→「Secrets」で設定します

その後にymlファイルを修正していきます

name: ANDROID_CI
on:
  push:
    branches:
      - develop
      - master
  pull_request:
    branches:
      - develop
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2

    - name: release build & sign
      run: |
        echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > release.keystore
        export KEYSTORE_PASSWORD="${{ secrets.KEYSTORE_PASSWORD }}"
        export KEY_ALIAS="${{ secrets.KEY_ALIAS }}"
        export KEY_PASSWORD="${{ secrets.KEY_PASSWORD }}"
        ./gradlew assembleRelease
    - name: upload release build
      uses: actions/upload-artifact@v2
      with:
        name: release_apk
        path: app

以上です

お役に立てると幸いです

今回も最後まで見てくださってありがとうございました

【GithubActions】ブランチ名に特定の文字列が入っているかどうか確認する

経緯

私の携わっているプロジェクトでは、release/〇〇.〇〇みたいな感じでバージョンごとにブランチ切ってるんですが、ブランチに記載しているバージョンとgredleに記載しているバージョンが違うというケースが発生しました

まぁ原因は私のバージョン累進忘れなんですけどね・・・

ということで、バージョンチェックもActionsでやろうということになったので今回はそのお話です

サンプル

name: ANDROID_CI
on:
  push:
    branches:
      - develop
      - release/**
      - master
  pull_request:
    branches:
      - develop
      - release/**
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
   
    - name: Set env to develop
      if: endsWith(github.ref, '/develop') || endsWith(github.base_ref , 'develop')
      run: echo "FLAVOR=develop" >> $GITHUB_ENV
    - name: Set env to release
      if: contains(github.ref, '/release') || contains(github.base_ref , 'release')
      # バージョン累進チェックを行う
      run: |
        if echo ${{github.ref}} | grep "${APK_VERSION}" > /dev/null || echo ${{github.base_ref}} | grep "${APK_VERSION}" > /dev/null ; then
          echo "FLAVOR=release" >> $GITHUB_ENV
        else
          exit 1
        fi
    - name: Set env to production
      if: endsWith(github.ref, '/master') || endsWith(github.base_ref , 'master')
      run: echo "FLAVOR=prod" >> $GITHUB_ENV
      ...

このようにgrep検索を条件にすることで実現できます

検索結果がある場合はそのまま環境変数を設定する処理を行い、なかったら「exit 1」で強制終了

grep部の変数はなぜかクオーテーションつけないと「unary operator expected」というエラーが発生したので必須のようです

/dev/nullに関してはなんのためにいるのかわかっていないですが、これがないと動かなかったです

参考:

blog.eiel.info

bash何もわからん

今回も最後まで見てくださってありがとうございました

【GithubActions】Githubの環境変数の設定・使用方法

経緯

ブランチ名をGithub変数に登録し、それをbash・アップリード処理で使用したいケースが起こりました

結構ややこしい部分なので、備忘録としてメモしておきます

サンプル

name: ANDROID_CI
on:
  push:
    branches:
      - develop
      - master
  pull_request:
    branches:
      - develop
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
          
    #事前設定
    - uses: actions/checkout@v2

    #環境変数に設定
    - name: Set env to develop
      if: endsWith(github.ref, '/develop') || endsWith(github.base_ref , 'develop')
      run: echo "FLAVOR=develop" >> $GITHUB_ENV
    - name: Set env to production
      if: endsWith(github.ref, '/master') || endsWith(github.base_ref , 'master')
      run: echo "FLAVOR=prod" >> $GITHUB_ENV

    #リリースビルド&署名(prodの場合はバージョン番号がAPK名になる)
    - name: release build & sign
      run: ./gradlew assemble${FLAVOR}_Release
    - name: upload release build
      uses: actions/upload-artifact@v2
      with:
        name: release_apk
        path: app/build/outputs/apk/${{env.FLAVOR}}_/release

こんな感じで使用することができます

echo "FLAVOR=develop" >> $GITHUB_ENV

とすると、github環境変数としてbash外でも使用できるようになります

階層の深さによって${〇〇}として使う場合と${env.〇〇}として使う場合があるので気をつけてください

ここで結構時間取られました・・・

今日は以上です

最後まで読んでいただいてありがとうございます

【GithubActions】前のJobの実施状況によって次のジョブを実施するか判定する

経緯

前のJobが終わったとき(スキップされていない場合)のみ実行したいケースが何回かありました

当然前のJobと同じ条件をコピペすればいいのですが、このケースが何回も続けば変更箇所が増えるのでメンテナンス性が落ちてしまいます

なんで、できるだけコピペじゃないやり方ないかなと探して見つかったのでまとめておきたいと思います

方法

見てもらったほうが早いと思うので、サンプルを記載します

name: ANDROID_CI
on:
  push:
    branches:
      - develop
      - master
  pull_request:
    branches:
      - develop
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: debug build
      if: endsWith(github.ref, '/develop') || endsWith(github.base_ref , 'develop')
      id: debug_build
      run: ./gradlew assembleDebug
    - name: upload debug build
      if: steps.debug_build.conclusion != 'skipped'
      uses: actions/upload-artifact@v2
      with:
        name: debug_apk
        path: app

デバッグビルドはdevelopブランチでのみ実施 そのビルド処理が走ったときだけアップロード処理を走らせたい、というケースになっています

手順としては、ベースとしたい処理にidを振り、次に実施する処理の分岐として、steps.(ID名).conclusion != 'skipped'を登録する

そうすることで、実行されたときのみ実施されるようになります

ちなみにsteps.(ID名).conclusion == 'skipped'とすればスキップされたときだけ実行されるので、疑似if-else文を書くことができます

まとめ

こちらもよくあるケースかなと思い、備忘録としてまとめておきたいと思います

お役に立てると幸いです

今回も最後まで見てくださってありがとうございました

【GithubActions】特定のブランチでのみ処理を実施したい場合にif文で分岐させるやり方

経緯

GithubActionsであるブランチへのPusu(PR)時のみ実施したい処理というのがいくつか出てきた

とはいえ、ブランチごとにymlファイルを作っちゃうと、変更するときの影響範囲が大きくなるのでやりたくない

一つのファイルの中で分岐させるようにしたい

方法

結論、containsやendsWithを利用することで条件分岐をさせることができる

ブランチの情報はgithub.refgithub.base_refで取得できる

github.ref

Push時のブランチ名を取得することができます

そのままブランチ名が出てくるわけではなく、refs/heads/(ブランチ名)という値になっています (タグのときはrefs/tags/(ブランチ名)となるようです)

なので、末尾が一致しているかどうかを判定するendsWithを使うことでチェックを行います

github.base_ref

PullRequest先のブランチ名を取得できます

これはgithub.refとちがってブランチ名をそのまま取得することができます

こちらはそのままイコールで比較すればいいんですが、めんどうなので同じようにendsWith使っています

endsWithとcontains

endsWithは上記のとおりですが、今回の私のケースだと末尾がバージョンになっていて、毎回名前が変わるという状況でした(例:release/〇〇.〇〇)

その時は文字列が含まれているかどうかを確認するcontainsを用います

この2つ以外にも、頭を比較するstartsWithもあります

サンプル

name: ANDROID_CI
on:
  push:
    branches:
      - develop
      - release/**
      - master
  pull_request:
    branches:
      - develop
      - release/**
      - master
    paths-ignore:
      - .idea/**
      - README.md

jobs:
  build:
    runs-on: ubuntu-latest
    steps:    
    - uses: actions/checkout@v2
    
    - name: only release and master
      if: contains(github.ref, '/release') || contains(github.base_ref , 'release') || endsWith(github.ref, '/master') || endsWith(github.base_ref , 'master')
      run: (実施したい処理)

まとめ

よくあるケースだとは思うのですが、案外調べて引っかからなかったので今回備忘録としてまとめておきました

お役に立てると幸いです

今回も最後まで見てくださってありがとうございました