🦁

FlutterアプリをNotionと連携する

2023/12/05に公開

Notionは史上最高の万能アプリなわけですが(異論は認める)、Flutterアプリ連携しちゃお〜ってやってたら案外苦労しました。というお話。

サンプルのリポジトリ公開しています!
https://github.com/Taichiro-S/notion_sample

前提

  • notionアカウント作成済み
  • flutterアプリ作成済み(fvmを使っているならこれが早いです)
  • OAuthの仕組みなんとなくわかる(この記事がわかりやすいかも)

実装する機能

FlutterアプリからNotionアカウントを連携し、Databaseにデータを書き込む

動作イメージ

※動画はiOS simulatorですが、androidでも同じように動作します。
アカウント連携
https://youtube.com/shorts/lesGbzy4NnQ

データベース操作
https://youtube.com/shorts/AfS8vTcOgQM

使用するFlutterパッケージ

  • freezed : immutableなクラスを作るやつ
  • riverpod : 状態管理のやつ
  • secure_storage : セキュアにストレージしてくれる
  • inappwebview : アプリ内でwebviewできる
  • http : httpリクエストするやつ
  • envied : 環境変数をセキュアに扱える

※これらのパッケージの使い方は解説しません!
自分がちゃんと使えているのかあやしいところがあるので...

環境・バージョン等

macOS Sonoma 14.1.1
iOS 17.0
Xcode 15.0.1
Dart SDK 3.2.2
Flutter 3.16.2

大まかな流れ

公式のドキュメントが割と丁寧に書いてあるので、これに沿って進めます。

  1. Notion integration作成
  2. リダイレクト用ページの作成
  3. OAuth認証フローの実装
  4. NotionのDatabaseにアクセス

長い道のりです。頑張りましょ〜😇

1. Notion integrationの作成

まずはここから、public integrationを作成します(internal integrationを作成し、ディストリビューションからパブリックに設定します)。
リダイレクトURIは、https://<アカウント名>.github.io/<リポジトリ名>/redirectsとします。後でGithub pagesでホスティングするURLになります。
設定が終わると、以下の3つが発行されるので、.envに書いておきます。

  • OAuthクライアントID
  • OAuthクライアントシークレット
  • 認証URL

lib/env/env.dartを作成し、enviedで.envファイルの中身を読み取ります。
https://github.com/Taichiro-S/notion_sample/blob/eccadea2d14239b3212100f4572fe28f490c6de7/lib/env/env.dart
build runnerを実行すると、env.g.dartが生成します。
env.g.dartは必ずgitignoreに記載しましょう。 ← ここ忘れるとenvの中身全部githubに晒してしまいます!!(一回やらかした人)

2. リダイレクト用ページの作成

Github pagesでホスティングし、notionからリダイレクトします。
直でアプリにリダイレクトできると楽なんですが、notionで設定するリダイレクトURLのスキームがhttpsしか設定できませんでした。
このページではリダイレクトURLに含まれるcodeを取得し、自分のアプリに再度リダイレクトします。

プロジェクトルートにdocs/redirects/index.htmlを作成します。
https://github.com/Taichiro-S/notion_sample/blob/eccadea2d14239b3212100f4572fe28f490c6de7/docs/redirects/index.html
URLにcodeが含まれないときのエラーページとしてfallback.htmlも作成しておきます。

mainブランチにpushし、githubのsettings → pages でmainブランチのdocsフォルダを選択します。

ブラウザからhttps://<アカウント名>.github.io/<リポジトリ名>/redirectsにアクセスしてfallback.htmlにリダイレクトされれば、ホスティングできています。

3. OAuth認証フローの実装

認証の大まかな流れは以下のようになります。

  1. 「連携」ボタンをクリックすると、integrationを作成したときの認証URLをinappwebviewで開く。
  2. ログインすると、integrationで設定したページにリダイレクトし、そこからアプリにリダイレクトする。
  3. リダイレクトURLに含まれるcodeを取り出して、アクセストークンを発行し、secure storageに保管する

以下の4つのファイルを作成します。

.
├── api
│   ├── notion_oauth_api.dart  <- apiリクエストを送ったりするクラス
│   └── notion_oauth_api.g.dart
├── env
│   ├── env.dart 
│   └── env.g.dart
├── main.dart  <- ログインページ
├── provider
│   ├── notion_auth_provider.dart  <- notionとの連携状態を管理するプロバイダ
│   ├── notion_auth_provider.freezed.dart
│   ├── notion_auth_provider.g.dart
│   ├── webview_provider.dart  <- webviewの状態を管理するプロバイダ
│   ├── webview_provider.freezed.dart 
│   └── webview_provider.g.dart
└── widget
    └── notion_login_webview_widget.dart   <- webviewの設定

1つずつ説明していきます。

  • main.dart : ログインページ
    webviewが開いている時はページを表示し、開いていないときはnotionとの連携状態に応じてボタンやnotionのデータ等を表示します。「連携」をクリックで認証ページを開き、「連携を解除」でsecure storageから認証情報を削除します。ただし、notionにはintegrationの連携が残ります。気になる場合は設定 → 自分のコネクトで連携を解除します。

https://github.com/Taichiro-S/notion_sample/blob/eccadea2d14239b3212100f4572fe28f490c6de7/lib/main.dart
このページでは以下2つのproviderをwatchしています。

  • notion_auth_provider.dart : notionとの連携状態を管理するprovider
    secure storageにアクセストークンがあれば連携しているとみなします。

https://github.com/Taichiro-S/notion_sample/blob/eccadea2d14239b3212100f4572fe28f490c6de7/lib/provider/notion_auth_provider.dart

  • webview_provider.dart : webviewの状態
    open, loading, errorの3つの状態があります。

https://github.com/Taichiro-S/notion_sample/blob/eccadea2d14239b3212100f4572fe28f490c6de7/lib/provider/webview_provider.dart

  • notion_oauth_api.dart : apiを送ったりするクラス
    リダイレクトURLに含まれるcodeを使用して、アクセストークンを発行するためのリクエストを行います。発行出来たらsecure storageに保存します。

https://github.com/Taichiro-S/notion_sample/blob/eccadea2d14239b3212100f4572fe28f490c6de7/lib/api/notion_oauth_api.dart

  • notion_login_webview_widget.dart : webviewの設定
    初期ページとしてintegration作成時の認証URLを設定しています。
    また、webview上でのリクエストで、自分のアプリに対するもの(Github pagesのリダイレクトURLに設定したnotionsample://oauth/callback?codeで始まるもの)があった場合、apiクラスのauthenticateメソッドを実行し、認証情報をsecure storageから取得します。

https://github.com/Taichiro-S/notion_sample/blob/eccadea2d14239b3212100f4572fe28f490c6de7/lib/widget/notion_login_webview_widget.dart

webviewの設定について2点補足説明します。

1. userAgentの設定について

今回はnotionにgoogleアカウントでログインするんですが、inappwebviewなどを使用した埋め込みブラウザでのOAuthリクエストはセキュリティ上の理由から許可されていません。なので、デフォルト設定ではdisallowed useragent というエラーになります。7年前になりますが公式のブログにも書かれています。

調べてみると、inappwebviewのuserAgentプロパティを利用してこの対策を回避することができるみたいです。
https://stackoverflow.com/questions/62730993/problem-in-google-login-in-canva-through-webview-in-flutter
userAgentを以下のように設定します。

InAppWebView(
    initialOptions: InAppWebViewGroupOptions(
        userAgent: Platform.isIOS
            ? 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_1_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Mobile/15E148 Safari/604.1'
            : 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Mobile Safari/537.36',
    ),
) 

url_launcherとuni_linksを使うのが正攻法ぽいですが、認証処理完了後にブラウザ画面をプログラム的に閉じることができない(ユーザが手動で閉じるしかない)ので、こちらの方法にしました。
もっといい方法を知っている方は教えてください!

2023/12/8 追記
url_launcherとuni_linksでOAuthを実装する記事を書きました(まだ期待通りの挙動ではない)
https://zenn.dev/xcter/articles/d6b94774c39f8d

InappwebviewパッケージのInAppBrowserというのも試してみたけど、結局userAgentを設定しないとはじかれるみたい。

2. androidの設定について

android emulatorでアプリを実行してみたところ、認証後のリダイレクトで以下のような画面が一瞬表示されました。

エラー内容的にはカスタムURLスキームが認識できていない?ようですが、すぐにアプリの画面に戻るので、どうなっているのかよくわかりません。

根本的な解決ではないですが、以下のようにエラーを非表示にしました。

initialOptions: InAppWebViewGroupOptions(
    android: AndroidInAppWebViewOptions(disableDefaultErrorPage: true)),

ただこれだとちゃんとしたエラーも出なくなってしまう...?
こちらも何かご存じの方は教えて欲しいです!

以上で認証処理の実装は完了です。

最後にGithub pagesからリダイレクトされた時にiOSとandroidアプリを開くように設定します。

iOSとandroideでのdeeplinkの設定

Github pagesからのリダイレクトでアプリを開くように設定します。
iOSは、iOS/Runner/Info.plistに以下を追記します。

Info.plist
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
    <key>CFBundleTypeRole</key>
    <string>Editor</string>
    <key>CFBundleURLSchemes</key>
    <array>
        <string>notionsample</string> <!-- 適宜カスタムURLスキームを設定 -->
    </array>
</dict>
</array>

androidはandroid/app/src/main/AndroidManifest.xmlのactivityタグ内に以下を追記します。

AndroidManifest.xml
<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="notionsampley"  />  <!-- 適宜カスタムURLスキームを設定 -->
</intent-filter>

これで認証フローの実装は完了です。

一旦休憩しましょ〜

4. Notionのデータベースにアクセス

認証が実装できたので、次はNotionのデータベースの読み取りと書き込みを行います。
操作できるのは認証操作の際にアクセスを許可したデータベースのみです。また、一度integrationを連携すると、notion側の設定からアクセスを許可するデータベースやページを変更できます。
こちらもドキュメントはしっかりしてますが、レスポンスの構造が中々ややこしいです...

以下のようにしてデータベースにアクセスします。

  1. データベースを名前で検索しIDを取得する(今回は「サンプル」という名前のデータベースにしています)
  2. データベースの「タイトル」プロパティを取得して表示する。
  3. タイトルプロパティを入力して新たなデータ行を挿入する。

以下の3つのファイルを追加します。

.
├── api
│   ├── notion_database_api.dart <- databaseエンドポイントにapiリクエストを送ったりするクラス
│   ├── notion_database_api.g.dart
│   ├── notion_oauth_api.dart
│   └── notion_oauth_api.g.dart
├── env
│   ├── env.dart
│   └── env.g.dart
├── main.dart
├── provider
│   ├── notion_auth_provider.dart
│   ├── notion_auth_provider.freezed.dart
│   ├── notion_auth_provider.g.dart
│   ├── notion_database_provider.dart <- 取得したデータを管理するプロバイダ
│   ├── notion_database_provider.freezed.dart
│   ├── notion_database_provider.g.dart
│   ├── webview_provider.dart
│   ├── webview_provider.freezed.dart
│   └── webview_provider.g.dart
└── widget
    ├── notion_database_list_widget.dart <- 取得したデータを表示するウィジェット
    └── notion_login_webview_widget.dart

1つずつ説明していきます。

  • notion_database_api.dart : databaseエンドポイントにapiリクエストを送ったりするクラス
    データベース名での検索、「タイトル」プロパティの取得、データの挿入の3つのメソッドを持つクラスです。認証時にアクセスを許可したデータベースのIDを取得するためのAPIエンドポイントがないようなので、名前で検索してIDを取得します。アクセストークンはsecure storageから取得します。

https://github.com/Taichiro-S/notion_sample/blob/eccadea2d14239b3212100f4572fe28f490c6de7/lib/api/notion_database_api.dart

  • notion_database_provider.dart : 取得したタイトルリストの状態管理

https://github.com/Taichiro-S/notion_sample/blob/eccadea2d14239b3212100f4572fe28f490c6de7/lib/provider/notion_database_provider.dart

  • notion_database_list_widge.dart : 入力欄と、取得したデータをリストで表示する

https://github.com/Taichiro-S/notion_sample/blob/eccadea2d14239b3212100f4572fe28f490c6de7/lib/widget/notion_database_list_widget.dart

認証直後はなぜかデータが取得できません!
数回リロードするとデータが読み込まれます。この挙動はチョットワカラン。

最後に

みんなもNotionを連携して使い倒そう!

参考にさせていただいた記事

https://zenn.dev/matsumaru/articles/fd63cf2793638f
https://qiita.com/yufuku/items/24dac97e6052b2571386

Discussion