FlutterアプリをNotionと連携する
Notionは史上最高の万能アプリなわけですが(異論は認める)、Flutterアプリ連携しちゃお〜ってやってたら案外苦労しました。というお話。
サンプルのリポジトリ公開しています!
前提
実装する機能
FlutterアプリからNotionアカウントを連携し、Databaseにデータを書き込む
動作イメージ
※動画はiOS simulatorですが、androidでも同じように動作します。
アカウント連携
データベース操作
使用する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
大まかな流れ
公式のドキュメントが割と丁寧に書いてあるので、これに沿って進めます。
- Notion integration作成
- リダイレクト用ページの作成
- OAuth認証フローの実装
- 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
ファイルの中身を読み取ります。
build runnerを実行すると、env.g.dart
が生成します。
env.g.dart
は必ずgitignore
に記載しましょう。 ← ここ忘れるとenvの中身全部githubに晒してしまいます!!(一回やらかした人)
2. リダイレクト用ページの作成
Github pagesでホスティングし、notionからリダイレクトします。
直でアプリにリダイレクトできると楽なんですが、notionで設定するリダイレクトURLのスキームがhttps
しか設定できませんでした。
このページではリダイレクトURLに含まれるcode
を取得し、自分のアプリに再度リダイレクトします。
プロジェクトルートにdocs/redirects/index.html
を作成します。
URLにcode
が含まれないときのエラーページとしてfallback.html
も作成しておきます。
mainブランチにpushし、githubのsettings → pages でmainブランチのdocsフォルダを選択します。
ブラウザからhttps://<アカウント名>.github.io/<リポジトリ名>/redirects
にアクセスしてfallback.html
にリダイレクトされれば、ホスティングできています。
3. OAuth認証フローの実装
認証の大まかな流れは以下のようになります。
- 「連携」ボタンをクリックすると、integrationを作成したときの認証URLをinappwebviewで開く。
- ログインすると、integrationで設定したページにリダイレクトし、そこからアプリにリダイレクトする。
- リダイレクト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の連携が残ります。気になる場合は設定 → 自分のコネクトで連携を解除します。
このページでは以下2つのproviderをwatchしています。
-
notion_auth_provider.dart
: notionとの連携状態を管理するprovider
secure storageにアクセストークンがあれば連携しているとみなします。
-
webview_provider.dart
: webviewの状態
open, loading, errorの3つの状態があります。
-
notion_oauth_api.dart
: apiを送ったりするクラス
リダイレクトURLに含まれるcode
を使用して、アクセストークンを発行するためのリクエストを行います。発行出来たらsecure storageに保存します。
-
notion_login_webview_widget.dart
: webviewの設定
初期ページとしてintegration作成時の認証URLを設定しています。
また、webview上でのリクエストで、自分のアプリに対するもの(Github pagesのリダイレクトURLに設定したnotionsample://oauth/callback?code
で始まるもの)があった場合、apiクラスのauthenticateメソッドを実行し、認証情報をsecure storageから取得します。
webviewの設定について2点補足説明します。
1. userAgentの設定について
今回はnotionにgoogleアカウントでログインするんですが、inappwebviewなどを使用した埋め込みブラウザでのOAuthリクエストはセキュリティ上の理由から許可されていません。なので、デフォルト設定ではdisallowed useragent というエラーになります。7年前になりますが公式のブログにも書かれています。
調べてみると、inappwebviewのuserAgentプロパティを利用してこの対策を回避することができるみたいです。
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を実装する記事を書きました(まだ期待通りの挙動ではない)
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
に以下を追記します。
<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タグ内に以下を追記します。
<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側の設定からアクセスを許可するデータベースやページを変更できます。
こちらもドキュメントはしっかりしてますが、レスポンスの構造が中々ややこしいです...
以下のようにしてデータベースにアクセスします。
- データベースを名前で検索しIDを取得する(今回は「サンプル」という名前のデータベースにしています)
- データベースの「タイトル」プロパティを取得して表示する。
- タイトルプロパティを入力して新たなデータ行を挿入する。
以下の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から取得します。
-
notion_database_provider.dart
: 取得したタイトルリストの状態管理
-
notion_database_list_widge.dart
: 入力欄と、取得したデータをリストで表示する
認証直後はなぜかデータが取得できません!
数回リロードするとデータが読み込まれます。この挙動はチョットワカラン。
最後に
みんなもNotionを連携して使い倒そう!
参考にさせていただいた記事
Discussion