📨

【Notion API+Ktor】MultiplatformアプリからNotionのDBにjsonデータを送信する

2023/10/23に公開

はじめに

Compose Multiplatformのアプリ(iOS/Android)で、ユーザーのフィードバックをアプリ内で入力・送信してもらい、受けられるようにしたい。

  • ずっと無料で
  • 楽に実装でき
  • DBとして保存できて
  • Slackに通知がいく

ようにしたい。
上記の要求をすべて満たすのが、Notion APIを使って、NotionのDBに送信するという方法でした。送信する形式は、汎用性の点からjson形式にしました。
意外とダイレクトな情報がなかったので、備忘録および同じようなことをしたい方の時短のために、記事にさせていただきます。

流れ

  1. データを受け取る準備 @Notion
  2. Notion APIキーの追加 @AndroidStudio
  3. Ktorの導入 @AndroidStuido
  4. 送信するデータの実装 @AndroidStudio
  5. データ送信のためのUI実装 @AndroidStudio
  6. Notionにデータを送信する実装 @AndroidStudio
  7. Slackに通知 @Notion/Slack

1. データを受け取る準備 @Notion

APIキーの取得

まず、Notion側にAPIを使用するための設定を行います。
Notionのインテグレーションページから、新しいインテグレーションを作成します。
すると、作成したインテグレーション固有のAPIキーを取得できます。(漏洩に注意)
secret_key

DBの作成

続いて、NotionにDB用のページを作成し、「テーブル」テンプレートを選択します。
「データソースを選択する」から、「新規データベース」を作成します。
続いて、データベースの各プロパティ名と形式を好きなように変更します。プロパティ名は、jsonで扱うときのキー名と一致している必要があります。

デフォルトのこれを、
db_before
例えばこう変更します。
db_after

DBにコネクトの追加

最初に作成したインテグレーションと、DBを紐づけます。画面右上のボタン(3つのドット)から「コネクトの追加」を選び、作成したインテグレーションを選択します。
db_integration

これでNotion側の準備は完了です。

2. Notion APIキーの追加 @AndroidStudio

Android Studioでの作業に移ります。

NotionのAPIキーを安全な形でアクセスできるようにします。
Multiplatformのプロジェクトなので、「BuildKonfig」を使いました。詳細は別記事にて説明予定です。

基本的に、値はlocal.propertiesに記述します。

local.properties
notion.api.key=NotionのAPIキー
notion.db.id=NotionのDBのID(DBのページをブラウザで開いた時の、?v=より前の32桁の値)

3. Ktorの導入 @AndroidStuido

Multiplatformで、HTTP通信クライアントとしてWizardにて推奨されている
Ktor」を導入します。

Ktorのバージョン2.3.5では、公式の通りに進めるとio.ktor:ktor-lient-cioがiOSにないよ、と怒られてしまいビルドできないのですが、Wizardを参考にするとAndrdoidとDesktopでは"io.ktor:ktor-client-okhttpを、iOSではio.ktor:ktor-client-darwinを使うようにすることで解消できるようです。

こちらも詳細は別記事にて。

4. 送信するデータの実装 @AndroidStudio

今回送信するデータは、以下のような形式を想定します。

形式 キー名 説明
title user_id ユーザーID
select evaluation 選択肢(Good/Bad)
rich-text message フィードバック内容

Notion APIのデータベースセクションを参考に進めます。
Database propertyページで、他の形式についても説明があります。

jsonに変換できるよう、kotlin-serializationを使ってデータクラスを作成していきます。Notionで送信されるjsonは、Body -> Parent,Properties という階層関係になっているため、それらも用意してあげます。

Body.kt
@Serializable
data class Body(
    val parent: Parent,
    val properties: Properties,
)

@Serializable
data class Parent(
    val database_id: String,
)

@Serializable
data class Properties(
    val user_id: UserId,
    val evaluation: Evaluation,
    val message: Message,
)
UserId.kt
@Serializable
data class UserId(
    val title: List<TitleText>,
)

@Serializable
data class TitleText(
    val text: Content,
)

@Serializable
data class Content(
    val content: String,
)
Evaluation.kt
@Serializable
data class Evaluation(
    val select: Select,
)

@Serializable
data class Select(
    val name: String,
)

enum class Usability() {
    Good,
    Bad,
    Soso,
}
Message.kt
@Serializable
data class Message(
    val rich_text: List<RichText>,
)

@Serializable
data class RichText(
    val text: RichTextValue,
    val plain_text: String,
    val href: String?,
)

@Serializable
data class RichTextValue(
    val content: String,
    val link: String?,
)

これで送信するデータの準備ができました。

5. データ送信のためのUI実装 @AndroidStudio

続いて、用意したデータクラスの中身をユーザーが入力できるようにするUIをこしらえます。
詳細はサンプルプロジェクトをご覧ください。UserIdの取得については別記事にて説明します。

入力内容の内部データクラスと、それの変換用拡張関数

FeedbackUiState.kt
data class FeedbackUiState(
    val userId: String = "",
    val evaluation: Usability = Usability.Good,
    val message: String = "",
)

fun FeedbackUiState.toBody(): Body {
    return Body(
        parent = Parent(database_id = BuildKonfig.NOTION_DB_ID),
        properties = Properties(
            user_id = UserId(
                title = listOf(
                    TitleText(
                        text = Content(
                            content = userId,
                        ),
                    ),
                ),
            ),
            evaluation = Evaluation(
                select = Select(
                    name = evaluation.name,
                ),
            ),
            message = Message(
                rich_text = listOf(
                    RichText(
                        text = RichTextValue(
                            content = message,
                            link = null,
                        ),
                        plain_text = message,
                        href = null,
                    ),
                ),
            ),
        ),
    )
}

入力画面のUI

FeedbackInputScreen.kt
@Composable
fun FeedbackInputScreen(
    modifier: Modifier = Modifier,
) {
    val uiState = remember { mutableStateOf(FeedbackUiState()) }
    var isMenuExpanded by remember { mutableStateOf(false) }
    val scope = rememberCoroutineScope()

    Column(modifier = modifier) {
        Text(text = "Evaluation")

        Box(
            modifier = Modifier
                .clickable { isMenuExpanded = true }
                .border(0.8.dp, MaterialTheme.colorScheme.onSurface, MaterialTheme.shapes.small)
                .width(200.dp)
        ) {
            Text(
                text = uiState.value.evaluation.name,
                modifier = Modifier.padding(8.dp),
            )
            DropdownMenu(
                expanded = isMenuExpanded,
                onDismissRequest = { isMenuExpanded = false },
                modifier = Modifier.width(200.dp)
            ) {
                Usability.values().forEach { usability ->
                    DropdownMenuItem(
                        text = { Text(text = usability.name) },
                        onClick = {
                            uiState.value = uiState.value.copy(evaluation = usability)
                            isMenuExpanded = false
                        })
                }
            }
        }

        Spacer(modifier = Modifier.height(16.dp))

        Text(text = "Message")
        OutlinedTextField(
            value = uiState.value.message,
            onValueChange = { uiState.value = uiState.value.copy(message = it) },
        )

        Button(onClick = {
            scope.launch(Dispatchers.IO) {
                KtorClient.sendFeedback(uiState.value.toBody(), afterAction = {
                    println("NotionResponse: sent successfully")
                    uiState.value = uiState.value.copy(message = "")
                })
            }
        }) {
            Text(text = "Send")
        }
    }
}

6. Notionにデータを送信する実装 @AndroidStudio

いよいよKtorのクライアントを使って、Notionにデータを送信する実装です。

まず、AndroidManifestに、Internetのパーミッションを追加しておきます。

AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />

以下が、Ktorでの送信処理です。Validation関連の実装は公式のガイドを参考にしました。

KtorClient.kt
object KtorClient {
    suspend fun sendFeedback(
        body: Body,
        afterAction: () -> Unit,
    ) {
        val response = HttpClient() {
            expectSuccess = true
            HttpResponseValidator {
                handleResponseExceptionWithRequest { exception, request ->
                    val clientException = exception as? ClientRequestException ?: return@handleResponseExceptionWithRequest
                    println("NotionResponse: ${clientException.response}")
                }
            }
        }.use { client ->
            client.post(
                urlString = "https://api.notion.com/v1/pages",
                block = {
                    headers {
                        append(HttpHeaders.Authorization, "Bearer BuildKonfig.NOTION_API_KEY")
                        append("Content-Type", "application/json")
                        append("Notion-Version", "2022-06-28")
                    }
                    setBody(Json.encodeToString(body))
                }
            )
        }

        if (response.status.isSuccess()) {
            afterAction()
        }
    }
}

ここで、アプリをビルドして、入力項目を埋めて送信すると、NotionのDBに保存されるはずです!
実際の画面の様子。
app_screenshot

うまくいくと、値が追加されているはずです。
db_added

7. Slackに通知 @Notion/Slack

最後に、NotionのDBに値が追加されたときに、Slackに通知がいくように設定します。

NotionのDBのページで、右上少し下の、「オートメーションボタン」(稲妻アイコン)をクリックし、新規オートメーションを作成します。
notion_automation

好きな名前をつけて、「トリガーを追加」に「ページを追加」を、「アクションを追加」に「Slack通知を送信」を選択します。
notion_automation_slack

Slackとの接続がまだの場合は、ここでSlackに接続のボタンが出るので、指示通りに進めてNotionとSlackを連携させます。
connect_notion_to_slack

無事に接続されると、Slackチャンネルが選択できるようになるので、通知したいチャンネルを選び、「作成」ボタンを押せば完了です。
notion_automation_created

おわりに

無料でここまで便利なことができてしまうって本当にすごい。感動です。
GitHub, Notion, Slack, Figmaの四天王が存在する以前と以降で、本当に世界は変わったと思います。

サンプルプロジェクトのGitHubリポジトリはこちらで公開しています。サクッと試してみたいときや、細かい確認用にどうぞ。

Discussion