セッションを使う
Supabaseにはセッションの仕組みを使うとログイン状態を維持することができる機能があるようです。
こちらのコードを使うと実現できます。
公式のログインを維持するコードが書かれたサンプルコード
import 'package:supabase_flutter/supabase_flutter.dart';
final supabase = Supabase.instance.client;
class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late final StreamSubscription<AuthState> _authSubscription;
User? _user;
void initState() {
_authSubscription = supabase.auth.onAuthStateChange.listen((data) {
final AuthChangeEvent event = data.event;
final Session? session = data.session;
setState(() {
_user = session?.user;
});
});
super.initState();
}
void dispose() {
_authSubscription.cancel();
super.dispose();
}
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
supabase.auth.signInWithOtp(email: 'my_email@example.com');
},
child: const Text('Login'),
);
}
}
他にも設定が必要な項目がいくつかありますが、公式を参考にしながら設定していこうと思います。
参考にしたサイト
完成品のソースコード
Redirect URLs
公式によると、ディープリンクなるものの設定が必要とのことです。
翻訳
依存関係のインストールが完了したので、マジックリンクまたは OAuth 経由でログインしたユーザーがアプリに戻れるように、ディープリンクを設定しましょう。
ダッシュボードに新しいリダイレクト URL として io.supabase.flutterquickstart://login-callback/ を追加します。
URLは何でもいいみたいです。公式のものをそのまま使わせてもらいました。こちらをコピーして、iOSとAndroidの設定で使用します。
このページでディープリンクを作成します。
iOSの設定をする
指定した場所にコードを追加してURLを設定
info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Supabase Notes</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>supabase_notes</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<!-- Add this array for Deep Links -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>io.supabase.flutterquickstart</string>
</array>
</dict>
</array>
<!-- ... other tags -->
</dict>
</plist>
Androidの設定をする
指定した場所にコードを追加してURLを設定
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.supabase_notes">
<application
android:label="supabase_notes"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Add this intent-filter for Deep Links -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
<data
android:scheme="io.supabase.flutterquickstart"
android:host="login-callback" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
これでOSごとの設定は終わりです。といっても私も手探りで設定したので、人によってはうまくいかないかもしれません?
-----
アプリのコード
今回は、ログインと新規登録してHelloWorldするだけなので、物足りないかもしれないですが、なかなか情報がないので、少しはお役に立てる記事になっているかなと思われます。
.envの設定ですが、必要ない方は、しなくても大丈夫です。
公式の紹介通りに設定してください。
.envの設定方法
URLとANON_KEYを設定する。
import 'package:supabase_flutter/supabase_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
url: SUPABASE_URL,
anonKey: SUPABASE_ANON_KEY,
);
runApp(MyApp());
}
// It's handy to then extract the Supabase client in a variable for later uses
final supabase = Supabase.instance.client;
.envを使った設定
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:supabase_notes/tutorial/my_widget.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// .envを読み込めるように設定.
await dotenv.load(fileName: '.env');
await Supabase.initialize(
url: dotenv.get('VAR_URL'), // .envのURLを取得.
anonKey: dotenv.get('VAR_ANONKEY'), // .envのanonキーを取得.
);
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyWidget(),
);
}
}
tutorialディレクトリにアプリのページを作成する。
ユーザーの新規登録とログインを行うページ。このページでログインをしているか判定をします。ログインしていれば、home_page.dartにアプリが起動したときに、リダイレクトします。ログインしていなかったらこちらのページが表示されます。
import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:supabase_notes/tutorial/home_page.dart';
// Supabaseをインスタンス化する.
final supabase = Supabase.instance.client;
class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
// ログインと新規登録に使用するTextEditingController.
TextEditingController _email = TextEditingController();
TextEditingController _password = TextEditingController();
//ログイン判定をする変数.
bool _redirecting = false;
//ユーザーの新規登録をするメソッド.
Future<void> signUpUser(String _email, String _password) async {
final AuthResponse res =
await supabase.auth.signUp(email: _email, password: _password);
log(res.toString());
}
//ログインをするメソッド.
Future<void> signInUser(String _email, String _password) async {
final AuthResponse res = await supabase.auth
.signInWithPassword(email: _email, password: _password);
log(res.toString());
}
late final StreamSubscription<AuthState> _authSubscription;
//セッションを使うための変数.
User? _user;
// ログインした状態を維持するためのロジック.
void initState() {
_authSubscription = supabase.auth.onAuthStateChange.listen((data) {
final AuthChangeEvent event = data.event;
final Session? session = data.session;
// ユーザーがログインしているかをifで判定する.
if (_redirecting) return;
if (session != null) {
_redirecting = true;
//ユーザーがログインしていたら、アプリのページへリダイレクトする.
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => HomePage()));
}
setState(() {
_user = session?.user;
});
});
super.initState();
}
void dispose() {
_authSubscription.cancel();
_email.dispose();
_password.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('LoginPage'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: _email,
decoration: InputDecoration(hintText: 'メールアドレスを入力'),
),
TextField(
controller: _password,
decoration: InputDecoration(hintText: 'パスワードを入力'),
),
ElevatedButton(
onPressed: () async {
// ログイン用ボタン
signInUser(_email.text, _password.text);
},
child: const Text('SignIn'),
),
ElevatedButton(
// 新規登録用のボタン.
onPressed: () async {
signUpUser(_email.text, _password.text);
},
child: const Text('SignUp'),
),
],
),
),
);
}
}
ログイン後のページ
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:supabase_notes/tutorial/my_widget.dart';
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () async {
// ログアウトするボタン.
await Supabase.instance.client.auth.signOut();
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => MyWidget()));
},
icon: Icon(Icons.logout)),
],
title: Text('home'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Wolcome!',
style: TextStyle(fontSize: 50),
)
],
),
),
);
}
}
新規登録をするとこのようなログが表示されます。
flutter: {"currentSession":{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjc3MjQyNTU1LCJzdWIiOiJlOTg0YmRjMS1hNmUwLTQwOWMtOGVhMC1hYmJlMWNmMjgwMjEiLCJlbWFpbCI6ImV4YW1wbGVAY28uanAiLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTY3NzIzODk1NX1dLCJzZXNzaW9uX2lkIjoiZWFjYjQ4YTgtZDEyNi00YzMyLWIzMjktNjM3OGJkYWQyMWZhIn0.CsXYoTlR8ZecMOE34O3dlZD5VEk0eMGuAic5AmfjbUE","expires_in":3600,"refresh_token":"qFHDEJAmbLmvFB1YE8_MWA","token_type":"bearer","provider_token":null,"provider_refresh_token":null,"user":{"id":"e984bdc1-a6e0-409c-8ea0-abbe1cf28021","app_metadata":{"provider":"email","providers":["email"]},"user_metadata":{},"aud":"authenticated","email":"example@co.jp","phone":"","created_at":"2023-02-24T11:42:35.120857Z","confirmed_at":null,"email_confirmed_at":"2023-02-24T11:42:35.1345691<…>
Reloaded 1 of 1235 libraries in 409ms (compile: 45 ms, reload: 230 ms, reassemble: 115 ms).
Reloaded 1 of 1235 libraries in 392ms (compile: 33 ms, reload: 235 ms, reassemble: 105 ms).
Reloaded 1 of 1235 libraries in 348ms (compile: 39 ms, reload: 192 ms, reassemble: 100 ms).
一度アプリを停止して、再起動させてみましょう。Welcomeと表示されていればログインの状態を維持しています。
アプリが起動するとこのようなログが表示されます。
flutter: {"currentSession":{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjc3MjQyNTU1LCJzdWIiOiJlOTg0YmRjMS1hNmUwLTQwOWMtOGVhMC1hYmJlMWNmMjgwMjEiLCJlbWFpbCI6ImV4YW1wbGVAY28uanAiLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTY3NzIzODk1NX1dLCJzZXNzaW9uX2lkIjoiZWFjYjQ4YTgtZDEyNi00YzMyLWIzMjktNjM3OGJkYWQyMWZhIn0.CsXYoTlR8ZecMOE34O3dlZD5VEk0eMGuAic5AmfjbUE","expires_in":3600,"refresh_token":"qFHDEJAmbLmvFB1YE8_MWA","token_type":"bearer","provider_token":null,"provider_refresh_token":null,"user":{"id":"e984bdc1-a6e0-409c-8ea0-abbe1cf28021","app_metadata":{"provider":"email","providers":["email"]},"user_metadata":{},"aud":"authenticated","email":"example@co.jp","phone":"","created_at":"2023-02-24T11:42:35.120857Z","confirmed_at":null,"email_confirmed_at":"2023-02-24T11:42:35.1345691<…>
flutter: ***** SupabaseDeepLinkingMixin startAuthObserver
ログアウトするとこのようなログが表示されます。
logout
flutter: ***** SupabaseDeepLinkingMixin startAuthObserver
flutter: **** onAuthStateChange: AuthChangeEvent.signedOut
ログインするとこのようなログが表示されます。
login
flutter: {"currentSession":{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjc3MjQzMDQ0LCJzdWIiOiJlOTg0YmRjMS1hNmUwLTQwOWMtOGVhMC1hYmJlMWNmMjgwMjEiLCJlbWFpbCI6ImV4YW1wbGVAY28uanAiLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTY3NzIzOTQ0NH1dLCJzZXNzaW9uX2lkIjoiNTgwZDgwNWQtM2Q3ZS00ZTRlLWE2ODYtMWM4NTg3NjE5OTg3In0.a658VnrX1OLWLrXZ83_4_WoaLUFkOfcVZcwH9VKj_xs","expires_in":3600,"refresh_token":"KpK1CdpU82VT7o_U4lbBcg","token_type":"bearer","provider_token":null,"provider_refresh_token":null,"user":{"id":"e984bdc1-a6e0-409c-8ea0-abbe1cf28021","app_metadata":{"provider":"email","providers":["email"]},"user_metadata":{},"aud":"authenticated","email":"example@co.jp","phone":"","created_at":"2023-02-24T11:42:35.120857Z","confirmed_at":"2023-02-24T11:42:35.134569Z","email_confirmed_at":"20<…>
最後に
あまり情報がないので、思いつきで作ってたまたまログインを維持する機能を実装できました。Supabaseは出たばかりの技術ですが、人気が出ていて今後伸びていく技術ではないかと思います。
認証機能を自分で作らなくても使えて、ログイン状態を維持できるので、これほど便利なサービスはないですね。
今年の3月21日の火曜日にFlutter大学のイベントで私、登壇する予定なのですが、SupabaseをテーマにしたFlutterの発表を行う予定です。
Discussion