コルーチン・フローを使ったAndroidサービスとの通信

# RemoteSharedFlow クラスモジュールの紹介

**参考記事：Kotlin Coroutine Flows and Android Services**

https://blog.stackademic.com/kotlin-coroutine-flows-and-android-services-af665f2a9f8f

**GitHub：ソースコード（ライブラリーとサンプル）**

https://github.com/mauricioandrada2021/remote-flow

## クライアントとサービス間で送信されるデータは文字列

**RemoteSharedFlow**では、クライアントとサービス間で送信されるデータとして文字列のみを使用するように設計されています。

```plantuml
@startuml
skinparam roundcorner 25

rectangle "クライアント"  as client <<Client>>
rectangle "サービス"  as service <<Service>>

client <.r.> service : コルーチン・フロー(文字列データ)

@enduml

```

これが選択された理由は、文字列を使用することの柔軟性です。

- クライアントとサービスはデータクラスを共有する必要がなく、データクラスの定義のみを共有すればよい
- 複雑なデータクラスであってもJSON文字列としてシリアル化・逆シリアル化できる
→ JSONは柔軟性があり、要素やフィールドが欠落している場合でもクラスを逆シリアル化できる
- バイナリデータでさえもBase64を使用して文字列に変換することで転送できる
- データクラス内のフィールドの順序を含む厳密なデータクラス定義を課すParcelableを使用する必要がない

# 使い方

リポジトリには、サンプルプログラム（**app**）が含まれています。

https://github.com/mauricioandrada2021/remote-flow/tree/main/app

## サービス

`MainService`クラスは、`RemoteSharedFlow`によって、バインドされるサービスです。
`onCreate()`関数で、`RemoteSharedFlow`のインスタンスを**引数無し**で取得し、`onBind()`関数で、バインダー(`asBinder`)を返しています。

```kotlin:MainService.kt
class MainService: Service() {

    private val tag = "Main Service"

    private lateinit var remoteSharedFlow: RemoteSharedFlow
    private val coroutineScope = CoroutineScope(Dispatchers.Default)

    override fun onCreate() {
        super.onCreate()
        remoteSharedFlow = remoteSharedFlow()
        coroutineScope.launch {
            remoteSharedFlow.flow().collect {
                println("$tag $it")
                remoteSharedFlow.emit("$tag Received: $it")
            }
        }
    }

    override fun onBind(intent: Intent?): IBinder {
        return remoteSharedFlow.asBinder()
    }

    override fun onDestroy() {
        super.onDestroy()
        coroutineScope.cancel()  // 本記事にて追加
    }
}
```

サービスとして、`onCreate()`関数の`coroutineScope.launch`の処理(Job)で、文字列データを受け取り、それを加工して送信（返信）しています。

```plantuml
participant MainService
participant Job
participant RemoteSharedFlow

MainService -> MainService : onCreate()
activate MainService
MainService --> RemoteSharedFlow
MainService --> Job : 開始
deactivate MainService
activate Job
group remoteSharedFlow.flow().collect()
    Job -> RemoteSharedFlow : 文字列データ取得
    RemoteSharedFlow ->> Job: 文字列データ it
    Job -> RemoteSharedFlow : 文字列データ送信 remoteSharedFlow.emit(）
end

MainService -> MainService : onDestroy()
activate MainService
MainService --> Job : 中断
deactivate Job

```

**【処理の中断】**
この処理(Job)は、永久的に繰り返されますので、`onDestroy()`関数内の`coroutineScope.cancel()`で、処理(Job)を中断するようにソースコードを追加しています（オリジナルのサンプルでは実装されていません）。

## クライアント

クライアントである`MainActivity`アクテビティで、`RemoteSharedFlow`を用いて、`MainService`サービスを起動し、サービスと通信を行っています。

クライアント側では、サービスを起動（バインド）するために、`remoteSharedFlow()`関数の引数をしていします。
- 第１引数: コンテキスト - MainActivity自身（this）
- 第２引数: パッケージ名 - "com.example.remoteflow"
- 第３引数: サービス名 - "com.example.remoteflow.MainService"

```kotlin: MainActivity.kt
class MainActivity : ComponentActivity() {

    private val tag = "Main Activity"

    private val coroutineScope = CoroutineScope(Dispatchers.Default)
    private lateinit var remoteSharedFlow: RemoteSharedFlow

    :
    
    override fun onStart() {
        super.onStart()
        remoteSharedFlow = remoteSharedFlow(
            this,
            "com.example.remoteflow",
            "com.example.remoteflow.MainService"
        )

        coroutineScope.launch {
            remoteSharedFlow.flow().collect {
                println("$tag Response1: $it")
            }
        }

        coroutineScope.launch {
            val counter = AtomicInteger(0)
            remoteSharedFlow.flow().collect {
                println("$tag Response2: $it")
                if (counter.incrementAndGet() == 10)
                    this.cancel()
            }
        }

        coroutineScope.launch {
            for (i in 1..100) {
                println("$tag sent: Hello there")
                remoteSharedFlow.emit("$tag Hello there")
                delay(2000)
            }
        }
    }
}
```

`onStart()`関数内で、３つの処理(Job1, Job2, Job3)を起動しています。

**１つ目の処理(Job1)**
サービスから受け取った文字列データを加工して、コンソール出力しています。

**２つ目の処理(Job2)**
１つ目の処理(Job1)と同様に、サービスから受け取った文字列データを加工して、コンソール出力していますが、10回目で、処理(Job2)を中断しています。

**３つ目の処理(Job2)**
２秒毎に100回、文字列データをサービスに送信しています。

### サービスの停止（バインド解除）

オリジナルの`RemoteSharedFlow`クラスでは、サービスを起動（バインド）することはできますが、停止（バインド解除）することができません。`RemoteSharedFlow`クラスを改良して、`serviceConnection`とのバインドを解除する手段を追加すると良いのかもしれません。

https://github.com/mauricioandrada2021/remote-flow/blob/main/remoteflow/src/main/java/com/example/remoteflowlib/RemoteSharedFlow_Impl.kt#L85-L87

```kotlin: バインドしている箇所 (RemoteSharedFlow_Impl.kt#L85-L87)
    if (!it.bindService(intent, serviceConnection, Service.BIND_AUTO_CREATE)) {
        it.unbindService(serviceConnection)
        throw BindException()
    }
```

```kotlin: バインドを解除する手段
    context.unbindService(serviceConnection)
```

# まとめ

`RemoteSharedFlow`を用いることにより、バインドタイプのサービスを起動することができ、クライアントとサーバーとで、文字列データの通信（双方向）が行えることを確認しました。