---
lang: ja
path: user-guide/android-sdk/quickstart-compose
labels: ユーザーガイド/Android SDK/クイックスタート(JetPack Compose)
metaTitle: クイックスタート(JetPack Compose) ｜ Android SDK ｜ ユーザーガイド ｜ SkyWay（スカイウェイ）
---

# 🚀 クイックスタート(JetPack Compose)

このセクションでは、SkyWay Android SDK を利用した最小限の JetPack Compose をベースとするアプリケーションを開発する方法について掲載しています。
はじめて Android SDK を利用する方はこちらを参考に導入してください。

完成品は [公式リポジトリ](https://github.com/skyway/android-sdk/tree/main/examples/JetpackCompose/QuickStart) にて公開しています。


尚、この記事は以下を前提に構成しています。
- JetPack Compose を用いたアプリ開発経験があること
- Kotlin の文法（関数、変数定義、呼び出し、コルーチンなど）がわかること


上記については Android Developers などを参考にしてください。

## アプリの概要

このアプリには以下のシンプルな通話機能をP2Pによって実装します。

1) ユーザーはルーム名を指定して入室できる
2) ユーザーがパーミッションの要求を許可すれば、入室と同時に同じルームのメンバーに対してカメラ映像とマイク音声を配信する
3) ユーザーは入室時から同じルームに入室しているユーザーのマイク音声とカメラ映像を視聴できる

## Android プロジェクトの作成

Android Developers の公式ウェブサイトを参考に、Android プロジェクトを作成します。

https://developer.android.com/codelabs/basic-android-kotlin-compose-first-app

「Minimum SDK」は Android 6.0(API Level: 23) 以降を選択してください。また、「Build configuration language」は kotlin DSL を選択してください。

## Android SDK のインストール

`gradle/libs.versions.toml` および `app/build.gradle.kts` に以下の依存関係を追加し、`Sync Project with Gradle Files` ボタンを押すだけでSDKのインストールは完了です。

libs.version.toml:
```shell
[versions]
agp = ...
# 追加
skyway = "x.x.x"
# 最新の SDK バージョンに差し替えてください
# 最新の SDK バージョン情報は以下のリンクより、Maven Central Repository にて確認できます
# https://central.sonatype.com/artifact/com.ntt.skyway/room

[libraries]
androidx-core-ktx = ...
# 追加
skyway-room = { group = "com.ntt.skyway", name = "room", version.ref = "skyway"}
```

build.gradle.kts:
```shell

android {
    ...
}

dependencies {
    ...

    // 追加
    implementation(libs.skyway.room)
}
```

## SkyWay Auth Token の作成

SkyWay を利用するためには、初めに JWT（JSON Web Token） を用いて `SkyWayContext` を初期化します。

SkyWay Auth Token は本来サーバーサイドで生成するため、Android SDK にはトークンの生成機能はございません。

クイックスタートでは、Dev環境専用のAPIである `SkyWayContext.setupForDev` を用いて初期化するため、SkyWay Auth Tokenの作成は省略します。

認証認可について、詳しくは[こちら](/ja/docs/user-guide/authentication/)をご覧ください。

## 機能の実装
### SkyWayContext, メンバの初期化
`MainActivity` と同じpackageにて、`MainViewModel` というクラスを新規に作成し、[ViewModel](https://developer.android.com/reference/androidx/lifecycle/ViewModel) を継承し、機能を実装します。

`MainViewModel` クラスから `SkyWayContext.Options` を設定し、先ほど生成された SkyWay Auth Tokenを記入します。
処理を `[1]` の部分に記入します。　　

また、後ほど必要になるメンバをまとめて宣言します。
処理を `[2]` の部分に記入します。

```kotlin
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ntt.skyway.core.SkyWayContext
import com.ntt.skyway.core.content.Stream
import com.ntt.skyway.core.content.local.LocalAudioStream
import com.ntt.skyway.core.content.local.LocalVideoStream
import com.ntt.skyway.core.content.local.source.AudioSource
import com.ntt.skyway.core.content.local.source.CameraSource
import com.ntt.skyway.core.content.remote.RemoteVideoStream
import com.ntt.skyway.core.util.Logger
import com.ntt.skyway.room.RoomPublication
import com.ntt.skyway.room.member.LocalRoomMember
import com.ntt.skyway.room.member.RoomMember
import com.ntt.skyway.room.Room
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.UUID

class MainViewModel(): ViewModel() {
    // [1] SkyWayContext.Optionsの設定
    private val option = SkyWayContext.Options(
        logLevel = Logger.LogLevel.VERBOSE
    )

    // [2] メンバの宣言
    var applicationContext: Context? = null
    private val appId = "YOUR_APP_ID"
    private val secretKey = "YOUR_SECRET_KEY"
    private var localRoomMember: LocalRoomMember? = null
    private var room: Room? = null
    var localVideoStream by mutableStateOf<LocalVideoStream?>(null)
        private set
    var localAudioStream by mutableStateOf<LocalAudioStream?>(null)
        private set
    var remoteVideoStream by mutableStateOf<RemoteVideoStream?>(null)
        private set

    // [3] Roomへの入室
}
```


### ユーザが入室する関数の作成

`MainViewModel` にて、メンバー入室操作を表す `joinAndPublish()` という関数を定義します。
処理を `[3]` の部分に記入します。
Android SDKの一部の機能はCoroutineでの非同期的な処理が必須となりますので、予め `viewModelScope` を用いてCoroutineを作成します。

```kotlin
class MainViewModel(): ViewModel() {
    ... 省略

    // [3] Roomへの入室
    fun joinAndPublish(roomName: String) {
        viewModelScope.launch() {
            // [4] SkyWayContextのセットアップ

            // [5] カメラからの映像取得と UI への表示

            // [6] マイクからの音声取得

            // [7] Room の作成

            // [8] Room への参加

            // [9] Stream の Publish と Subscribe
        }
    }
}
```


### SkyWayContextのセットアップ

MainViewModel にて、`SkyWayContext` をセットアップします。
処理を `[4]` の部分に記入します。

> SkyWayContext.setupForDev はdev環境での利用が想定されているAPIです。
> 本番環境では、SecretKeyを秘匿するためSkyWayContext.setupをご利用ください。

```kotlin
class MainViewModel(): ViewModel() {
    ... 省略

    // [3] Roomへの入室
    fun joinAndPublish(roomName: String){
        viewModelScope.launch() {
            // [4] SkyWayContextのセットアップ
            val result = SkyWayContext.setupForDev(applicationContext!!, appId, secretKey, option)
            if (result) {
                Log.d("App", "Setup succeed")
            }
        }
    }
}


```

### カメラからの映像取得と UI への表示

先ほどに続いて、同じCoroutine内で、映像入力を取得しUIへ反映します。
処理を `[5]` の部分に記入します。

```kotlin
class MainViewModel(): ViewModel() {

    fun joinAndPublish(roomName: String){
        viewModelScope.launch() {
            // [4] SkyWayContextのセットアップ
            ... 省略

            // [5] カメラからの映像取得と UI への表示
            // cameraリソースの取得
            val device = CameraSource.getFrontCameras(applicationContext!!).first()

            // camera映像のキャプチャを開始します
            val cameraOption = CameraSource.CapturingOptions(800, 800)
            CameraSource.startCapturing(applicationContext!!, device, cameraOption)

            // MainThreadで LocalVideoStreamを更新します
            withContext(Dispatchers.Main) {
                // 描画やpublishが可能なStreamを作成します
                // localVideoStream Stateが更新され、MainScreenのSurfaceViewRendererより描画します
                localVideoStream = CameraSource.createStream()
            }
        }
    }
}
```

### マイクからの音声取得

音声を取得します。
処理を `[6]` の部分に記入します。
AudioSource は静的な object であることに注意してください。

```kotlin
class MainViewModel(): ViewModel() {

    fun joinAndPublish(roomName: String){
        viewModelScope.launch() {
            // [5] カメラからの映像取得と UI への表示
            ... 省略

            // [6] マイクからの音声取得
            // 音声入力を開始します
            AudioSource.start()

            // publishが可能なStreamを作成します
            localAudioStream = AudioSource.createStream()
        }
    }
}
```

### Room の作成

Streamの作成に続けて、入退室可能な `Room` を作成します。
処理を `[7]` の部分に記入します。
今回はユーザーが編集可能な `roomName` 要素を部屋の名前として利用します。

```kotlin
class MainViewModel(): ViewModel() {

    fun joinAndPublish(roomName: String){
        viewModelScope.launch() {
            // [6] マイクからの音声取得
            ... 省略

            // [7] Room の作成
            room = Room.findOrCreate(name = roomName)
        }
    }
}
```

### Room への参加

続いて `Room` に参加し、 `publish` / `subscribe` が可能な `LocalRoomMember` を取得します。
処理を `[8]` の部分に記入します。
ユーザー名は `Room` 内でユニークである必要があります。今回はUUIDを用いてランダムに決定しています。
`resultMessage` には `Room` への参加の成否を格納し、結果を `Toast` によるポップアップで通知しています。

```kotlin
class MainViewModel(): ViewModel() {

    fun joinAndPublish(roomName: String){
        viewModelScope.launch() {
            // [7] Room の作成
            ... 省略

            // [8] Room への参加
            val memberInit = RoomMember.Init(name = "member_" + UUID.randomUUID())
            localRoomMember = room?.join(memberInit)

            val resultMessage = if (localRoomMember == null) "Join failed" else "Joined room"
            withContext(Dispatchers.Main) {
                Toast.makeText(applicationContext, resultMessage, Toast.LENGTH_SHORT).show()
            }
        }
    }
}
```

### Stream の Publish

`Room` に入室しているあるメンバーから他のメンバーへ `Stream` を公開(`publish`)をします。
処理を `[9]` の部分に記入します。
これにより、他のメンバーは対象の `Stream` を購読( `subscribe` )することが可能になります。 `subscribe` は次の項で実装します。

また、 `Room` にハンドラを追加することで `publish` 時に特定の動作を行わせることが可能です。

```kotlin
class MainViewModel(): ViewModel() {

    fun joinAndPublish(roomName: String){
        viewModelScope.launch() {
            // [8] Room への参加
            ... 省略

            // [9] Stream の Publish と Subscribe
            // ハンドラ
            room?.onStreamPublishedHandler = {
                // [9.1] このRoom内で誰かがPublishするたびに実行される
            }

            // [9.2] 入室時に他のメンバーのStreamを購読する

            // 映像、音声のPublish
            localRoomMember?.publish(localVideoStream!!)
            localRoomMember?.publish(localAudioStream!!)
        }
    }
}
```

ハンドラの登録は `publish()` との順序が重要です。例えば `publication` が追加されたことをハンドラで検知する場合は、上記のように、**予め** `Room` にハンドラを追加する必要があります。

### Stream の Subscribe

`Room` に入室中のメンバーが公開( `publish` )している stream を購読( `subscribe` )します。
処理を `[10]` の部分に記入します。
また、`[9.1]` と `[9.2]` の部分に、購読関数を呼び出します。　　

> 自身で `publish` した `stream` は `subscribe` できないことに注意してください。

```kotlin
class MainViewModel(): ViewModel() {
    ... 省略

    fun joinAndPublish(roomName: String) {
        viewModelScope.launch(){
            // [8] Room への参加
            ... 省略

            // [9] Stream の Publish と Subscribe
            // ハンドラ
            room?.onStreamPublishedHandler = Any@{ //ここの Any@ の追加も忘れなく
                // [9.1] このRoom内で誰かがPublishするたびに実行される
                Log.d("room", "onStreamPublished: ${it.id}")
                if (it.publisher?.id == localRoomMember?.id) {
                    return@Any
                }
                subscribe(it)
            }

            // [9.2] 入室時に他のメンバーのStreamを購読する
            room?.publications?.forEach {
                if (it.publisher?.id == localRoomMember?.id) return@forEach
                subscribe(it)
            }

            localRoomMember?.publish(localVideoStream!!)
            localRoomMember?.publish(localAudioStream!!)
        }
    }

    // [10] 室中のメンバーが公開しているstreamを購読する
    private fun subscribe(publication: RoomPublication) {
        viewModelScope.launch {
            // Publicationをsubscribeします
            val subscription = localRoomMember?.subscribe(publication)
            subscription?.stream?.let { stream ->
                if (stream.contentType == Stream.ContentType.VIDEO) {
                    withContext(Dispatchers.Main) {
                        // remoteVideoStream Stateが更新され、MainScreenのSurfaceViewRendererより描画します
                        remoteVideoStream = subscription.stream as RemoteVideoStream
                    }
                }
            }
        }
    }
}
```
なお、 `AudioStream` の場合は `subscribe` を完了したタイミングでスピーカーから音声が流れます。

## レイアウトの設定

`MainActivity` と同じpackageにて、`MainScreen.kt` という新規ファイルを作成します、表示したいUIコンポーネントを `@Composalbe` でマークされた関数として記述します、またUI表示に関する変数も定義します。

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.ntt.skyway.core.content.local.LocalVideoStream
import com.ntt.skyway.core.content.remote.RemoteVideoStream
import com.ntt.skyway.core.content.sink.SurfaceViewRenderer
import java.util.UUID

@Composable
fun MainScreen(
    mainViewModel: MainViewModel,
    modifier: Modifier,
) {
    // Room名表示state
    var roomName by remember { mutableStateOf(UUID.randomUUID().toString()) }

    // 映像表示state
    val localVideoStream: LocalVideoStream? = mainViewModel.localVideoStream
    val remoteVideoStream: RemoteVideoStream? = mainViewModel.remoteVideoStream
    var localRenderView by remember { mutableStateOf<SurfaceViewRenderer?>(null) }
    var remoteRenderView by remember { mutableStateOf<SurfaceViewRenderer?>(null) }

    Column(modifier = Modifier.fillMaxSize()) {
        // [11] Video 表示コンポーネントの配置

        // [12] ルーム名の表示枠と参加ボタンの作成
    }
}
```

### Video 表示コンポーネントの配置

"ローカルのビデオ"には自身の映像、"リモートのビデオ"には通話相手の映像を映します。

処理を `[11]` の部分に記入します。

```kotlin

@Composable
fun MainScreen(
    mainViewModel: MainViewModel,
    modifier: Modifier
) {
    ... 省略

    Column(modifier = Modifier.fillMaxSize()) {
        // [11] Video 表示コンポーネントの配置
        Row(
            horizontalArrangement = Arrangement.Center,
            modifier = Modifier
                .fillMaxWidth()
                .height(200.dp)
        ) {
            // ローカルのビデオ
            if (localVideoStream != null) {
                AndroidView(
                    modifier = Modifier
                        .width(150.dp)
                        .height(150.dp),
                    factory = { context ->
                        localRenderView = SurfaceViewRenderer(context)
                        localRenderView!!.apply {
                            setup()
                            localVideoStream.addRenderer(this)
                        }
                    },
                    update = {
                        localVideoStream.removeRenderer(localRenderView!!)
                        localVideoStream.addRenderer(localRenderView!!)
                    }
                )
            }
            // リモートのビデオ
            if (remoteVideoStream != null) {
                AndroidView(
                    modifier = Modifier
                        .width(150.dp)
                        .height(150.dp),
                    factory = { context ->
                        remoteRenderView = SurfaceViewRenderer(context)
                        remoteRenderView!!.apply {
                            setup()
                            remoteVideoStream.addRenderer(this)
                        }
                    },
                    update = {
                        remoteVideoStream.removeRenderer(remoteRenderView!!)
                        remoteVideoStream.addRenderer(remoteRenderView!!)
                    }
                )
            }
        }

        // [12] ルーム名の表示枠と参加ボタンの作成

    }
}
```

### ルーム名の表示枠と参加ボタンの作成
ルーム名の表示枠、または参加ボタンを追加し、参加ボタンのonClick 関数にて、 `mainViewModel.joinAndPublish()` 関数を呼び出します。
処理を `[12]` の部分に記入します。

```kotlin

@Composable
fun MainScreen(
    mainViewModel: MainViewModel,
    modifier: Modifier
) {
    ... 省略

    Column(modifier = Modifier.fillMaxSize()) {
        // [11] Video 表示コンポーネントの配置
        Row() {
            ... 省略
        }

        // [12] ルーム名の表示枠と参加ボタンの作成
        Row(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
            Text("room:")
            TextField(
                value = roomName,
                onValueChange = { newText ->
                    roomName = newText
                },
            )
        }
        Row(
            horizontalArrangement = Arrangement.Center,
            modifier = Modifier
                .fillMaxWidth()
        ) {
            Button(
                onClick = {
                    // Roomへの参加処理
                    mainViewModel.joinAndPublish(roomName)
                }
            ) {
                Text("Join")
            }
        }
    }
}
```

### UIの初期化
`MainActivity` に以下を実装します：
- `[13]` の部分では、`MainViewModel` をActivityのメンバー変数として定義し、初期化しています。
- `[14]` の部分では、Activityから取得される `applicationContext` を `mainViewModel` に渡します。
- `[15]` の部分では、FullScreen正しく表示させるために設定を行います。
- `[16]` の部分では、`MainScreen` を呼び出して、UI初期化を行なっています。

```kotlin
import androidx.compose.foundation.layout.safeDrawingPadding

class MainActivity : ComponentActivity() {
    // [13] viewModelの定義および初期化
    private val mainViewModel = MainViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        // [14] contextのセットアップ
        // SkyWayの諸APIを呼び出すにはapplicationContextが必要
        mainViewModel.applicationContext = applicationContext

        setContent {
            // Android Project作成される際にProject名によって自動的に生成されるTheme名となります
            // Project名によって異なります
            SkyWayComposeQuickStartAppTheme {
                Scaffold(modifier = Modifier
                    .fillMaxSize()
                    // [15] FullScreen対応
                    .safeDrawingPadding()
                ) { innerPadding ->
                    // [16] MainScreenの呼び出し
                    // 本来のGreeting関数を削除し、代わりにMainScreen関数を呼び出します
                    MainScreen(
                        mainViewModel = mainViewModel,
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }

        // [17] 権限の要求
    }
}
```

## パーミッションの設定

Android SDK の動作に必要なパーミッションを `app/src/main/AndroidManifest.xml` に記述します。

以下に、映像・音声の通信をする際に必要なパーミッションの例を示します。

```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <!-- ネットワーク接続に必要なパーミッション  -->
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

    <!-- カメラ映像・マイク音声の取得に必要なパーミッション  -->
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>

    <!-- CAMERA パーミッションのWarningを消すため -->
    <uses-feature android:name="android.hardware.camera" android:required="false" />

    <application>
        ...
    </application>

</manifest>
```

### パーミッションの要求

カメラやマイクから入力を受け取る前に、アプリケーションにはデバイスを使う権限が必要です。
処理を `[17]` の部分に記入します。

```kotlin

import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker

class MainActivity : ComponentActivity() {
    ... 省略

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

        // [17] 権限の要求
        if (ContextCompat.checkSelfPermission(
                applicationContext,
                android.Manifest.permission.CAMERA
            ) != PermissionChecker.PERMISSION_GRANTED ||
            ContextCompat.checkSelfPermission(
                applicationContext,
                android.Manifest.permission.RECORD_AUDIO
            ) != PermissionChecker.PERMISSION_GRANTED
        ) {
            ActivityCompat.requestPermissions(
                this,
                arrayOf(
                    android.Manifest.permission.CAMERA,
                    android.Manifest.permission.RECORD_AUDIO,
                ),
                0
            )
        }
    }
}
```

## 動作確認

Android Studio の Run ボタンを押下してアプリケーションを実行してください。

## 次のステップ

今回はメディアを `publish`,`subscribe` するシンプルな例でした。

その他のサンプルアプリケーションはこちらをご参照ください。

[サンプルコード](/ja/docs/sample-code/android-sdk/)

また、開発の前に開発ドキュメントもご一読ください。

[Android SDKの開発ドキュメント](/ja/docs/user-guide/android-sdk/)
