Quickstart: Flutter
Build the notes app in Flutter: initialise Sovaria with a secure session store, switch screens on sign-in, and list and add the signed-in user's notes.
A Flutter app for iOS and Android where people sign up, sign in, add notes and see only their own.
#Before you start
Do steps 1 to 3 of the Quickstart first: create a project, copy its project URL and anon key (Quickstart), and create the notes table with its row rules (Quickstart). Every signed-in user then sees and changes only their own notes, whatever the app code does.
#1. Create the app and install
flutter create notes
cd notes
flutter pub add sovaria flutter_secure_storage#2. Initialise Sovaria with a secure session store
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:sovaria/sovaria.dart';
import 'notes_app.dart';
/// Keeps the session in the Keychain (iOS) or Keystore (Android).
class SecureStore implements SessionStorage {
final _s = const FlutterSecureStorage();
@override
Future<String?> read(String key) => _s.read(key: key);
@override
Future<void> write(String key, String value) => _s.write(key: key, value: value);
@override
Future<void> delete(String key) => _s.delete(key: key);
}
late final Sovaria sovaria;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Restores the saved session (refreshing it if it's about to expire).
sovaria = await Sovaria.initialize(
url: 'https://<project-ref>.sovarias.in',
anonKey: '<anon-key>',
storage: SecureStore(),
);
runApp(const NotesApp());
}#3. Sign in and work with notes
import 'package:flutter/material.dart';
import 'package:sovaria/sovaria.dart';
import 'main.dart';
class NotesApp extends StatelessWidget {
const NotesApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
home: StreamBuilder<AuthState>(
// The first event is initialSession with the restored session (or null).
stream: sovaria.auth.onAuthStateChange,
builder: (context, snap) => snap.data?.session == null ? const SignInPage() : const NotesPage(),
),
);
}
class SignInPage extends StatefulWidget {
const SignInPage({super.key});
@override
State<SignInPage> createState() => _SignInPageState();
}
class _SignInPageState extends State<SignInPage> {
final email = TextEditingController(), password = TextEditingController();
String error = '';
Future<void> run(Future<Object?> Function() action) async {
try {
await action();
} on SovariaException catch (e) {
setState(() => error = e.message);
}
}
@override
Widget build(BuildContext context) => Scaffold(
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
TextField(controller: email, decoration: const InputDecoration(labelText: 'Email')),
TextField(controller: password, obscureText: true, decoration: const InputDecoration(labelText: 'Password')),
FilledButton(
onPressed: () => run(() => sovaria.auth.signInWithPassword(email: email.text, password: password.text)),
child: const Text('Sign in'),
),
TextButton(
onPressed: () => run(() => sovaria.auth.signUp(email: email.text, password: password.text)),
child: const Text('Create account'),
),
if (error.isNotEmpty) Text(error),
]),
),
);
}
class NotesPage extends StatefulWidget {
const NotesPage({super.key});
@override
State<NotesPage> createState() => _NotesPageState();
}
class _NotesPageState extends State<NotesPage> {
final body = TextEditingController();
List<Map<String, Object?>> notes = [];
@override
void initState() {
super.initState();
load();
}
// Rows come back as maps keyed by column name. The row rules add "where owner_id = auth.uid()".
Future<void> load() async {
final result = await sovaria.db.sql('select id, body from notes order by id desc');
setState(() => notes = result.rows);
}
Future<void> add() async {
await sovaria.db.sql(r'insert into notes (body) values ($1)', [body.text]);
body.clear();
await load();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Notes'), actions: [
IconButton(icon: const Icon(Icons.logout), onPressed: () => sovaria.auth.signOut()),
]),
body: Column(children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(children: [
Expanded(child: TextField(controller: body, decoration: const InputDecoration(hintText: 'Write a note'))),
IconButton(icon: const Icon(Icons.add), onPressed: add),
]),
),
Expanded(
child: ListView(children: [for (final n in notes) ListTile(title: Text('${n['body']}'))]),
),
]),
);
}- Use a raw string (
r'…') for SQL with$1so Dart doesn't read it as interpolation. signUpcreates the user and signs them in, unless the project asks people to confirm their email first (Email and password).
#4. Run it
flutter run#Next steps
- Check who can read and change
notesbefore you ship: Tutorial: prove your table is private before you ship. - Push new notes to other devices as they happen: Database change events.
- Deep links, push and attestation: Flutter quickstart.
- Every type and method: Dart SDK.
#Coming from Firebase or Supabase?
| Platform | What this is called there |
|---|---|
| Firebase | FlutterFire |
| Supabase | Supabase Flutter quickstart |
| Sovaria | Quickstart: Flutter |
Moving an existing app? Migrate from Supabase or Firebase imports users (with their passwords), data, files and rules.