🔥

[KMP]Android+iOSでFirebase Storageから画像を取ってくるまで

2024/09/21に公開

環境

  • macOS Sonoma14.5
  • Android Studio Koala Feature Drop | 2024.1.2
  • Xcode Version 15.4

Kotlin Multiplatformの環境構築については他に記事がそれなりにあるので既にできているものとして省略します。

また、全体コードはGitHubで公開しています。
https://github.com/arashiyama11/FirebaseExample

何を作るか

サンプルプロジェクトとして、Firebase Storageから画像を取ってきて表示するシンプルなAndroid/iOSアプリを実装します。

Android iPhone

プロジェクト作成

まず https://kmp.jetbrains.com でプロジェクトの作成を行います
以下命名について少し触れますが、完全に個人の見解です。

Project Name,Project IDの命名

Project Nameはandroid,iOSのアプリ名,iOSのBundle IDの一部に利用されます
英数字、ハイフン、アンダースコアが利用可能ですが、Bundle IDがアンダースコア利用不可なので特段こだわりがなければアンダースコアは利用しないのが無難です。アンダースコアを利用してもBundle IDが書かれているファイルでアンダースコアをハイフンなどに置き換えれば問題はないと思われます。

Project IDはandroidのパッケージ名やiOSのBundle IDに利用されます。なので逆ドメイン形式で書きましょう。同じく英数字、ハイフン、アンダースコアが利用可能ですが、同じ理由でアンダースコアは利用しないのが無難です。

Downloadボタンをクリックしてそのフォルダをandroid studioなどで開くとビルドが走ります。

Firebase側の設定

Firebaseのプロジェクトを作成するとこんな感じの画面になります。

Andriodアプリの登録

droid君のアイコンをタップすることでAndroidアプリの登録をできます。

package名はProject IDと同じです。
アプリを登録を押すとこんな画面になります。

google-service.jsonをダウンロードします。
このファイルはcomposeApp/に置いてください

次にFirebase SDKの追加が求められますが、後にFirebase SDKをKMPに対応させたライブラリGitLiveApp/firebase-kotlin-sdkの依存関係の追加とともに記述するので一旦飛ばします。

iOSアプリの登録

同様にしてiOSアプリを登録しようとするとBundle IDが求められます。
Bundle IDはiosApp/Configuration/Config.xcconfigに書いてあります。

iosApp/Configuration/Config.xcconfig
TEAM_ID=
BUNDLE_ID=com.example.firebase.example.FirebaseExample
APP_NAME=FirebaseExample

このBUNDLE_IDをコピペしてください

アプリを登録を押すと設定ファイルのダウンロードが求められます。

これをiosApp/iosApp/に貼り付けるのですが、ここで必ずXcodeを用いて貼り付けてください。つまりFinderでダウンロードフォルダからGoogleService-Info.plistをXcodeにドラッグして貼り付けて下さい。FinderやAndroid Studio上で移動してもXcodeはそのファイルを認識しません。
XcodeはiosApp/iosApp.xcodeprojを右クリックして以下のように開くことができます

Xcodeを開いてFinderからファイルをiosApp/iosAppにドラッグすると以下の画面が出ます

ここでCopy items if needed にチェックを付けて下さい。付けなくても動きはしますが、付けなかった場合XcodeがDownloadフォルダにあるオリジナルのファイルを参照し続けます。つまりDownloadフォルダにあるオリジナルのファイルを消すと動かなくなってしまいます。

貼り付けるとこんな感じになります。

iOS依存関係の設定

Firebaseに戻って次へボタンをクリックするとFirebase SDKの追加が求められます。ここはFirebaseに表示されている指示に従ってXcodeのFile→Add package dependencesを開いて

右上の検索欄にfirebase-ios-sdkと入力すると

と出るのでfirebase-ios-sdkをクリックして右下のAdd Packagesをクリックします。
こんな感じの画面が出てくるので必要なもののAdd to TargetをiosAppにして右下のAdd Packageをクリックして下さい。今回はStorageのみを利用します。

iOSのFirebase初期化コードの追加

iosApp/iosApp/iosApp.swiftを以下に変更します。

iosApp/iosApp/iosApp.swift
import SwiftUI
import Firebase

@main
struct iOSApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }

    class AppDelegate:NSObject,UIApplicationDelegate{
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
            FirebaseApp.configure()
            return true
        }
    }
}

Xcode部分はこれにて終了です。

Firebase Storageの設定

Firebaseから構築→Storageから設定をして下さい。ルールの設定は期限付きで誰でもread可能なルールにしました。また、いらすとやから適当な画像をアップロードしました。

依存関係の追加

Xcodeは閉じてAndroid Studioを開いて下さい
今回は画像の表示ということでCoilも利用します。
(2ファイル書かなければならないのがめんどくさいのでバージョンカタログは利用しません。)

google-serviceのプラグインを追加します。

build.gradle.kts
plugins {
    // this is necessary to avoid the plugins to be loaded multiple times
    // in each subproject's classloader
    alias(libs.plugins.androidApplication) apply false
    alias(libs.plugins.androidLibrary) apply false
    alias(libs.plugins.jetbrainsCompose) apply false
    alias(libs.plugins.compose.compiler) apply false
    alias(libs.plugins.kotlinMultiplatform) apply false
+   id("com.google.gms.google-services") version "4.4.2" apply false
}

firebase,coil,ktorを追加します。ktorはcoil3でネットから画像を取得するのに必要だそうです。
(参照: https://coil-kt.github.io/coil/upgrading_to_coil3/ )

composeApp/build.gradle.kts
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.androidApplication)
    alias(libs.plugins.jetbrainsCompose)
    alias(libs.plugins.compose.compiler)
+   id("com.google.gms.google-services")
}

kotlin {
    androidTarget {
        @OptIn(ExperimentalKotlinGradlePluginApi::class)
        compilerOptions {
            jvmTarget.set(JvmTarget.JVM_11)
        }
    }
    
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "ComposeApp"
            isStatic = true
        }
    }
    
    sourceSets {
        androidMain.dependencies {
            implementation(compose.preview)
            implementation(libs.androidx.activity.compose)
+           implementation("io.ktor:ktor-client-okhttp:3.0.0-beta-2")
        }
        commonMain.dependencies {
            implementation(compose.runtime)
            implementation(compose.foundation)
            implementation(compose.material)
            implementation(compose.ui)
            implementation(compose.components.resources)
            implementation(compose.components.uiToolingPreview)
            implementation(libs.androidx.lifecycle.viewmodel)
            implementation(libs.androidx.lifecycle.runtime.compose)
+           implementation("dev.gitlive:firebase-common:2.1.0")
+           implementation("dev.gitlive:firebase-storage:2.1.0")

+           implementation("io.ktor:ktor-client-core:3.0.0-beta-2")

+           implementation("io.coil-kt.coil3:coil:3.0.0-alpha10")
+           implementation("io.coil-kt.coil3:coil-compose:3.0.0-alpha10")
+           implementation("io.coil-kt.coil3:coil-network-ktor3:3.0.0-alpha10")
        }
+       iosMain.dependencies {
+           implementation("io.ktor:ktor-client-darwin:3.0.0-beta-2")
+       }
    }
}

android {
    namespace = "com.example.firebase.example"
    compileSdk = libs.versions.android.compileSdk.get().toInt()

    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    sourceSets["main"].res.srcDirs("src/androidMain/res")
    sourceSets["main"].resources.srcDirs("src/commonMain/resources")

    defaultConfig {
        applicationId = "com.example.firebase.example"
        minSdk = libs.versions.android.minSdk.get().toInt()
        targetSdk = libs.versions.android.targetSdk.get().toInt()
        versionCode = 1
        versionName = "1.0"
    }
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    buildFeatures {
        compose = true
    }
    dependencies {
        debugImplementation(compose.uiTooling)
    }
}

Compose Multiplatformのコード

Firebaseから画像ダウンロードのURLを取得は以下のようにできます

Firebase.storage.reference.listAll().items.map { it.getDownloadUrl() }

これをAsyncImageに渡せば今回のアプリは完成です。
以下commonMainのApp.ktの全体コードです。

App.kt
package com.example.firebase.example

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.storage.storage
import org.jetbrains.compose.ui.tooling.preview.Preview


@Composable
fun App() {
    var urls by remember { mutableStateOf(emptyList<String>()) }
    LaunchedEffect(Unit){
        urls=Firebase.storage.reference.listAll().items.map { it.getDownloadUrl() }
    }
    MaterialTheme {
        LazyVerticalGrid(
            columns = GridCells.Adaptive(180.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp),
            modifier = Modifier.fillMaxSize().padding(horizontal = 4.dp),) {
            items(urls){
                AsyncImage(
            it,
                    contentDescription = null,
                    contentScale = ContentScale.Crop,
                    modifier = Modifier.fillMaxWidth()
                )
            }
        }
    }
}

これで完成です。(再掲)

Android iPhone

Discussion