FlutterでSupabaseを使う
ノートアプリを作って使い方を学んでみた!
こちらが公式
Firebase好きな人が作ったRDBをFirebaseのように使えるサービス。文字情報以外に、画像のアップロードや認証機能など便利な機能を提供してくれいる。
とは言ってもまだ、試作品らしいです。でも世界では人気が出ているようです。
タイラーさんというSupabaseに詳しい方のチュートリアルで学習しました。
やること
タイラーさんの動画を参考にプロジェクトを作成してデータベースを作成します。
そこに、notesテーブルを作成します。
URLとanon keyをコピーしておいてください。
アプリにURLとanon keyを設定する。
でもこのままだと、API KEYが見えちゃうので環境変数で隠します😅
こちらのpackageをアプリに追加しておいてください。
.envの設定をするプロジェクト直下に.envファイルを作成して、.gitignoreにコミットの対象から外す設定を追加する。
先ほどコピーしたURLとanon keyを.envファイルに設定してください。
VAR_URL='https://自分のURLsupabase.co'
VAR_ANONKEY='自分のアノンキーa1X-pETbt8Rj6hydP9DgJb5R7v40QSAmCkg-TJP0Hl8'
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# .envをコミットの対象外にする.
.env
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
name: supabase_app
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: '>=2.18.6 <3.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
flutter_dotenv: ^5.0.2
supabase_flutter: ^1.2.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
assets:
- .env # .envの設定を追加.
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
main.dartにタイラーさんのアプリを少し改良したアプリを作成しました。
使える機能は、追加・表示・更新・削除です。
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:supabase_flutter/supabase_flutter.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(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyNotes(),
);
}
}
class MyNotes extends StatefulWidget {
MyNotes({Key? key}) : super(key: key);
State<MyNotes> createState() => _MyNotesState();
}
class _MyNotesState extends State<MyNotes> {
// Streamでリアルタイムにデータを取得する.
final _noteStream =
Supabase.instance.client.from('notes').stream(primaryKey: ['id']);
// Formの値を保存するTextEditingController.
TextEditingController _body = TextEditingController();
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Notes'),
), // StreamBuilderで、画面に描画する.
body: StreamBuilder<List<Map<String, dynamic>>>(
stream: _noteStream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
final notes = snapshot.data!;
return ListView.builder(
itemCount: notes.length,
itemBuilder: (context, index) {
return ListTile(
trailing: SizedBox(
width: 100,
child: Row(
children: [
IconButton(
onPressed: () async {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: const Text('Add a Note'),
contentPadding:
const EdgeInsets.symmetric(
horizontal: 1.0),
children: [
TextFormField(
controller: _body,
),
ElevatedButton(
onPressed: () async {
// Formから取得したデータを更新する.
await Supabase.instance.client
.from('notes')
.update({
'body': _body.text
}).match({
'id': notes[index]['id']
});
Navigator.of(context).pop();
},
child: Text('Put'))
],
);
});
},
icon: Icon(
Icons.edit,
color: Colors.blueAccent,
)),
IconButton(
onPressed: () async {
// Listのデータを受け取りMapでindexから、選択したリストのidを取得する.
// ボタンを押すとクエリが実行されて、データが削除される!
await Supabase.instance.client
.from('notes')
.delete()
.match({'id': notes[index]['id']});
},
icon: Icon(
Icons.delete,
color: Colors.redAccent,
),
),
],
),
),
title: Text(notes[index]['body']), // Mapでbodyデータを取得.
subtitle: Text(notes[index]['created_at']),// 作成された日時を取得.
);
},
);
}),
floatingActionButton: FloatingActionButton(
onPressed: () {
// showDialogのFormからデータをPostする.
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: const Text('Add a Note'),
contentPadding: const EdgeInsets.symmetric(horizontal: 1.0),
children: [
TextFormField(
controller: _body,
),
ElevatedButton(
onPressed: () async {
// Formから取得したデータを保存する.
await Supabase.instance.client
.from('notes')
.insert({'body': _body.text});
Navigator.of(context).pop();
},
child: Text('Post'))
],
);
});
},
child: const Icon(Icons.add),
),
);
}
}
Demoアプリを使ってみる
まずは、追加をやってみます。
データが保存できたらリアルタイムにデータが表示されます。
次は更新をやってみます。データが書きかわっていたら成功。
次は削除をやってみます。ゴミ箱のボタンを押して、テーブルからデータが消えたら成功。
まとめ
Supabaseを使ってみた感想ですが、RDSをFirebaseLikeに使えるサービスでした。
フロントエンドとバックエンド両方を作ると、サーバー借りて、データベース作って、接続する処理が必要なんですけど、外部サービスを使えば、設定して決まったコード書くだけで簡単なCRUDできるアプリなら、30分ぐらいで作れました。
データの変更がデータベースに反映されるのも速いので、快適ですね。個人開発で使ってみたいなと思いました。Supabase皆さんも試してみてください。面白いですよ!
おまけ
コードをリファクタリングしてみました。これでもまだ良いコードとは思えないですが😅
処理を関数化した機能が入ったデータクラス。
import 'package:supabase_flutter/supabase_flutter.dart';
// 追加・更新・削除の処理を使えるクラス.
class DataBaseService {
// ゲッターを作成して、インスタンス化したSupabaseClientを渡す.
SupabaseClient get client => Supabase.instance.client;
// データを追加するメソッド.
Future<void> addNotes(String _body) async {
// Formから取得したデータを保存する.
await client.from('notes').insert({'body': _body});
}
// データを更新するメソッド.
Future<void> updateNotes(dynamic noteID, String _body) async {
await client.from('notes').update({'body': _body}).match({'id': noteID});
}
// データを削除するメソッド.
Future<void> deleteNotes(dynamic noteID) async {
await client.from('notes').delete().match({'id': noteID});
}
}
コードを修正したmain.dart
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:supabase_app/model/node_model.dart';
import 'package:supabase_flutter/supabase_flutter.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(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyNotes(),
);
}
}
class MyNotes extends StatefulWidget {
MyNotes({Key? key}) : super(key: key);
State<MyNotes> createState() => _MyNotesState();
}
class _MyNotesState extends State<MyNotes> {
// Streamでリアルタイムにデータを取得する.
final _noteStream =
Supabase.instance.client.from('notes').stream(primaryKey: ['id']);
// Formの値を保存するTextEditingController.
TextEditingController _body = TextEditingController();
void dispose() {
// TODO: implement dispose
_body.dispose();
super.dispose();
}
Widget build(BuildContext context) {
final service = DataBaseService();
return Scaffold(
appBar: AppBar(
title: const Text('My Notes'),
), // StreamBuilderで、画面に描画する.
body: StreamBuilder<List<Map<String, dynamic>>>(
stream: _noteStream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
final notes = snapshot.data!;
return ListView.builder(
itemCount: notes.length,
itemBuilder: (context, index) {
return ListTile(
trailing: SizedBox(
width: 100,
child: Row(
children: [
IconButton(
onPressed: () async {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: const Text('Put a Note'),
contentPadding:
const EdgeInsets.symmetric(
horizontal: 1.0),
children: [
TextFormField(
controller: _body,
),
ElevatedButton(
onPressed: () async {
final noteID = notes[index]['id'];
// Formから取得したデータを更新する.
// await Supabase.instance.client
// .from('notes')
// .update({
// 'body': _body.text
// }).match({
// 'id': noteID
// });
service.updateNotes(
noteID, _body.text);
Navigator.of(context).pop();
},
child: Text('Put'))
],
);
});
},
icon: Icon(
Icons.edit,
color: Colors.blueAccent,
)),
IconButton(
onPressed: () async {
final noteID = notes[index]['id'];
// Listのデータを受け取りMapでindexから、選択したリストのidを取得する.
// ボタンを押すとクエリが実行されて、データが削除される!
// await Supabase.instance.client
// .from('notes')
// .delete()
// .match({'id': notes[index]['id']});
service.deleteNotes(noteID);
},
icon: Icon(
Icons.delete,
color: Colors.redAccent,
),
),
],
),
),
title: Text(notes[index]['body']), // Mapでbodyデータを取得.
subtitle: Text(notes[index]['created_at']), // 作成された日時を取得.
);
},
);
}),
floatingActionButton: FloatingActionButton(
onPressed: () {
// showDialogのFormからデータをPostする.
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: const Text('Add a Note'),
contentPadding: const EdgeInsets.symmetric(horizontal: 1.0),
children: [
TextFormField(
controller: _body,
),
ElevatedButton(
onPressed: () async {
// Formから取得したデータを保存する.
// await Supabase.instance.client
// .from('notes')
// .insert({'body': _body.text});
service.addNotes(_body.text);
Navigator.of(context).pop();
},
child: Text('Post'))
],
);
});
},
child: const Icon(Icons.add),
),
);
}
}
Discussion