ビルドバリアントごとに処理の実装を切り替えたい場合に発生したコンフリクトを解消する方法
ビルドバリアントでは、mainディレクトリに置いてあるファイルとの差分の内容を、ビルドバリアントのディレクトリに配置することで、そのビルドバリアントを選択してビルドした際に優先的に読み込まれるのかと思っていた。
結論から言うと、ビルドタイプの名称をもつディレクトリを作成し、mainディレクトリと構成の齟齬が無いように差分のファイルを配置することが必要です。
Androidのビルドバリアントをイチから理解する | フューチャー技術ブログ
これを踏まえて、soda
ビルドタイプとcola
ビルドタイプを作成してみた。
buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } debug { applicationIdSuffix ".debug" debuggable true } cola { applicationIdSuffix ".cola" debuggable true } soda { applicationIdSuffix ".soda" debuggable true } }
そして、次のようにmain/
ソースセットにGreeting.kt
を配置した。
package com.takagimeow.material3catalog.components class Greeting { fun greet() = "Main Greeting" }
ファイル階層は次の通り。
- src
- main
- java
- com.takagimeow.material3catalog
- components
- Greeting.kt
- components
- com.takagimeow.material3catalog
- java
- main
そして、soda
ビルドタイプ固有のファイルを作成するために次のディレクトリ階層を作成した。
- src
- soda
- java
- com.takagimeow.material3catalog
- components
- Greeting.kt
- components
- com.takagimeow.material3catalog
- java
- soda
Greeting.kt
の内容は次の通り。
package com.takagimeow.material3catalog.components import androidx.compose.material3.Text import androidx.compose.runtime.Composable class Greeting { fun greet() = "Soda Greeting" }
すると、次のようなRedeclaration: Greeting
エラーが発生した。
色々調べた結果、実装をビルドバリアント毎に変えたいクラスやコンポーザブルなどのリリース用の実装は、main/
ソースセットに配置するのではなく、新たにrelease
ビルドタイプ固有のファイルを作成する必要があることがわかった。
gradle - Error Redeclaration class configuring Android build variants - Stack Overflow
なので、main/
ソースセットに配置したGreeting.kt
は一旦削除する。そして、release
ビルドタイプ固有のファイルとしてGreeting.kt
を新たに作成する。
- src
- release
- java
- com.takagimeow.material3catalog
- components
- Greeting.kt
- components
- com.takagimeow.material3catalog
- java
- release
これにより、Redeclaration
エラーは解消される。
試しに、上記で定義したGreeting
クラスのgreet()
をMainActivity
で呼び出してみる。
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.d("MainActivity", "${Greeting().greet()}") setContent { Material3CatalogTheme { // A surface container using the 'background' color from the theme Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { Greeting(stringResource(id = R.string.app_name)) } } } } }
[Build] -> [Select Build Variant]に移動して、ビルドバリアントをsodaに切り替えて実行してみると、soda
ビルドタイプで定義した内容が優先的に実行されるようになる。
実行した結果、LogcatにSoda Greeting
と表示されていることがわかる。
今度は、cola
ビルドバリアントに変更して実行してみることにする。
もちろんcola
ビルドタイプ固有のGreeting.kt
を作成していないので、Unresolved reference
エラーが発生する。
このように、ビルドバリアントごとに実装を切り替えたい場合はその実装をmain
ディレクトリには配置せずにrelease
ディレクトリを作成してその中に本番用の実装を定義するようにすることで、同じパッケージ名でファイルを作成したとしてもエラーを回避することができるようになる。
参考
KotlinのExposedで作成日に基づいて並び替えた値に対してページネーションを行いたい場合
ExposedのDAO APIを使って次のようなモデルを定義してみる。
object Posts : UUIDTable() { val text = varchar("text", 144) val createdAt = datetime("created_at") } class Post(id: EntityID<UUID>) : UUIDEntity(id) { companion object : UUIDEntityClass<Post>(Posts) var text by Posts.text var createdAt by Posts.createdAt }
この時ページネーションを行いたいときに、次のような条件を考えてみる。
- 取得数とオフセットを使って取得する範囲を特定する。
- 日付を基に降順でデータを取得したい。
色々調べて思いついたのが、sortedByDescending()
を使ってみること。
override suspend fun limitedPosts(n: Int, offset: Long): List<PostRecord> { return transaction { Post.all().limit(n, offset).sortedByDescending { post -> post.createdAt }.map { it.toPostRecord() } } }
ただ、これだとlimit()
で既に取得した値に対してソートを行った結果を取得することになってしまう。
これを回避するために、orderBy()
とlimit()
を組み合わせる。
override suspend fun limitedPosts(isActive: Boolean, n: Int, offset: Long): List<PostRecord> { return transaction { Post.all().orderBy(Posts.createdAt to SortOrder.DESC).limit(n, offset) .filter { post -> post.isActive == isActive } .map { it.toPostRecord() } } }
これにより、ソートされた結果に基づいてlimit()
を呼び出した内容を取得することができる。
参考
How does sorting work with limit in kotlin exposed model? - Stack Overflow
通知に関する実行時の権限を要求するダイアログが表示されない時に考えるべきこと
launchPermissionRequest()
を呼び出して、通知に関する権限の確認を行いたい場合がある。
val permission = Manifest.permission.POST_NOTIFICATIONS val permissionState = rememberPermissionState(permission = permission) when { permissionState.status.isGranted -> { viewModel.updateIsNotificationEnabled(it) } else -> { permissionState.launchPermissionRequest() } }
本来想定しているのは、次のようにダイアログが表示されている状態。
しかし、これが表示されない場合があり、次のようなエラーが発生する。
None of [android.permission.POST_NOTIFICATIONS] in {} ... Input channel object '6xxxxx com.google.android.permissioncontroller/com.android.permissioncontroller.permission.ui.GrantPermissionsActivity (client)' was disposed without first being removed with the input manager!
実は、通知に関する実行時の権限はAPIレベル33から導入されたもの。
通知に関する実行時の権限 | Android デベロッパー | Android Developers
なので、APIレベル32以下のAndroid 12などの端末やエミュレーターでいくら権限を要求してもダイアログは表示されない。
そのため、顕現を要求する際は現在の実行環境のSDKバージョンを確認してから行うと良い。
import android.os.Build ... when { Build.VERSION.SDK_INT >= 33 -> { when { permissionState.status.isGranted -> { ... } else -> { permissionState.launchPermissionRequest() ... } } } else -> { ... } }
参考
How can I check the system version of Android? - Stack Overflow
java - Android permission doesn't work even if I have declared it - Stack Overflow
Cloud Runにデプロイを試みた時に発生したPermission 'run.services.get' denied on resourceエラーの解決方法
GitHub Actionsからサービスアカウントを使ってCloud Runにデプロイを行なった時に次のようなエラーが発生した。
ERROR: (gcloud.run.deploy) PERMISSION_DENIED: Permission 'run.services.get' denied on resource
このエラーの内容からgcloud.run.deploy
の権限が付与されていないためエラーが発生しているように見えるが、このときサービスアカウントにはすでにCloud Run管理者
権限が付与されている状態だった。
この場合の解決方法としては、Cloud Buildのページから設定を開く。
Cloud Run 管理者のステータスを有効に切り替える。
すべてのサービスアカウントにアクセス権を付与をクリックする。
この状態で、GitHub Actionsからもう一度デプロイを試みると解決した。
ワークフローファイルの内容は次の通り。
name: Deploy on: push: branches: - main workflow_dispatch: jobs: deploy: runs-on: ubuntu-latest timeout-minutes: 60 needs: [test] if: ${{ (github.ref == 'refs/heads/main') }} steps: - name: Checkout uses: actions/checkout@v3 - name: Set up JDK 11 uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: 11 - name: Make gradlew executable run: chmod +x ./gradlew - name: Setup Gradle uses: gradle/gradle-build-action@v2 - id: 'auth' uses: 'google-github-actions/auth@v1' with: credentials_json: '${{ secrets.GCP_CREDENTIALS }}' - name: Set up gcloud CLI uses: google-github-actions/setup-gcloud@v1 - name: Configure Docker with Credentials run: | gcloud auth configure-docker - name: Build the Docker Image run: | gcloud config set project ${{ secrets.GCP_PROJECT_NAME }} gcloud builds submit --config ./cloudbuild.yaml
参考
Cloud Build を使用した継続的デプロイの手動設定 | Cloud Run のドキュメント | Google Cloud
ViewModelを初期化しようとして`Module with the Main dispatcher had failed to initialize`というエラーが出た話
テスト時にViewModel
を初期化する処理を次のように書いた。
class LabelListViewModelTest { private val testScheduler = TestCoroutineScheduler() private val dispatcher = StandardTestDispatcher(testScheduler) private val repository = mockk<LabelRepository>(relaxed = true) private val sut = LabelListViewModel(repository) @Before fun setup() { Dispatchers.setMain(dispatcher) }
そしたら、次のようなエラーが発生した。
Exception in thread "Test worker" java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
ViewModel
の中身を確認してみる。
... @HiltViewModel class LabelListViewModel @Inject constructor( private val labelRepository: LabelRepository ) : ViewModel() { private val viewModelState = MutableStateFlow( LabelListViewModelState( isLoading = false, ) ) val uiState = viewModelState .map { it.toUiState() } .stateIn( viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState() )
おそらく、setup()
でDispatchers.setMain()
を呼び出す前に、.stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState())
が呼び出されてしまっているためModule with the Main dispatcher had failed to initialize
というエラーが発生したのだと思う。
なのでlateinit
を使ってsetup()
の中でsut
を初期化するようにしたらエラーが消えた。
... @HiltViewModel class LabelListViewModel @Inject constructor( private val labelRepository: LabelRepository ) : ViewModel() { private val viewModelState = MutableStateFlow( LabelListViewModelState( isLoading = false, ) ) val uiState = viewModelState .map { it.toUiState() } .stateIn( viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState() )
`builder: Modifier.() -> Modifier`の読み方
DroidKaigi 2022のコードを眺めていると次のような記述にでくわした。
inline fun Modifier.ifTrue(value: Boolean, builder: Modifier.() -> Modifier): Modifier { val modifier = Modifier return then(if (value) modifier.builder() else modifier) }
このbuilder: Modifier.() -> Modifier
をどう解釈すればよいのかわからなかったので、深夜にツイートしてみた。
Kotlinの`builder: Modifier.() -> Modifier`の読み方がわからにゃい・・・Modifierの拡張関数でModifierを返す関数って解釈でいいのかな・・・
— たかぎとねこ (@takagimeow) 2022年11月27日
すると、@ji_sungbinさんからリプが!
そうだね
— 성빈 🌸 (@ji_sungbin) 2022年11月27日
なので、ここでのbuilder
はModifier
の拡張関数であり、Modifier
を返り値として返す関数という意味らしい。
たまにこういう記述を見かける時があってその都度思考停止してたから、スッキリした。
追伸
Android界隈はとても優しいエンジニアの方が多い印象があって、本当に楽しいし大好き😊
ワークフローのファイルを作成してリモートリポジトリにプッシュしようとしたら失敗した
.github/workflows/Build.yaml
を作成して、リモートリポジトリにプッシュしようとしたらエラーが発生した。
! refs/heads/ci-build-workflow:refs/heads/ci-build-workflow [remote rejected] (refusing to allow a Personal Access Token to create or update workflow `.github/workflows/Build.yaml` without `workflow` scope)
意訳すると、Workflowのスコープが現在ログインに使用しているトークンに含まれていないということだ。
トークンの作成時に、workflows
スコープにチェックを入れてからトークンを新たに作成する。
作成が完了したら、画面に表示されているトークンをコピーしておく。
IntelliJ IDEAで環境設定を開き、左のメニューからバージョン管理、GitHubの順番にクリックする。 ログイン済みの場合は、既存のアカウントを削除する。
トークンでログインをクリックする。
先ほどコピーしたトークンを貼り付けて、アカウントの追加をクリックする。
自分のアカウントが正しく表示されていれば完了。
あとは、もう一度プッシュしてみて成功するかを確認する。