apps/docs/docs/ref/dart/v0/upgrade-guide.mdx
supabase-flutter focuses on improving the developer experience and making it easier to use. This guide demonstrates how to upgrade from supabase-flutter v0 to v1.
<RefSubLayout.EducationRow> <RefSubLayout.Details>
Update the package in your pubspec.yaml file.
</RefSubLayout.Details> <RefSubLayout.Examples>
supabase_flutter: ^1.0.0
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
<RefSubLayout.EducationRow> <RefSubLayout.Details>
The way supabase-flutter throws error has changed in v1. In v0, errors were returned as a response. In v1, errors are thrown as exceptions. This makes it more intuitive as a Flutter developer to handle errors.
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
final res = await supabase.from('my_table').select().execute();
final error = res.error;
if (error != null) {
// handle error
}
final data = res.data;
try {
final data = supabase.from('my_table').select();
} catch (error) {
// handle error
}
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
SupabaseAuthState and SupabaseAuthRequiredState classes<RefSubLayout.EducationRow> <RefSubLayout.Details>
In v0, SupabaseAuthState and SupabaseAuthRequiredState were required to handle automatic token refresh and to listen to auth state change. In v1, SupabaseAuthState and SupabaseAuthRequiredState are deprecated, and token refresh will happen automatically just by initializing Supabase. onAuthStateChange can be used to action on auth state change.
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
await Supabase.initialize(
url: 'SUPABASE_URL',
anonKey: 'SUPABASE_ANON_KEY',
);
...
class AuthState<T extends StatefulWidget> extends SupabaseAuthState<T> {
...
}
...
class AuthRequiredState<T extends StatefulWidget> extends SupabaseAuthState<T> {
...
}
await Supabase.initialize(
url: 'SUPABASE_URL',
anonKey: 'SUPABASE_ANON_KEY',
);
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
<RefSubLayout.EducationRow> <RefSubLayout.Details>
onAuthStateChange now returns a Stream.
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
final authSubscription = supabase.auth.onAuthStateChange((event, session) {
// handle auth state change
});
// Unsubscribe when no longer needed
authSubscription.data?.unsubscribe();
final authSubscription = supabase.auth.onAuthStateChange.listen((data) {
final AuthChangeEvent event = data.event;
final Session? session = data.session;
// handle auth state change
});
// Unsubscribe when no longer needed
authSubscription.cancel();
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
<RefSubLayout.EducationRow> <RefSubLayout.Details>
signIn() has been deprecated in favor of more explicit method signatures to help with type hinting. Previously it was difficult for developers to know what they were missing (e.g., a lot of developers didn't realize they could use passwordless magic links).
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
await supabase.auth.signIn(email: email, password: password);
await supabase.auth.signInWithPassword(email: email, password: password);
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
<RefSubLayout.EducationRow> <RefSubLayout.Details>
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
await supabase.auth.signIn(email: email);
await supabase.auth.signInWithOtp(email: email);
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
<RefSubLayout.EducationRow> <RefSubLayout.Details>
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
await supabase.auth.signInWithProvider(
Provider.github,
options: AuthOptions(
redirectTo: kIsWeb
? null
: 'io.supabase.flutter://reset-callback/'),
);
await supabase.auth.signInWithOAuth(
Provider.github,
redirectTo: kIsWeb ? null : 'io.supabase.flutter://reset-callback/',
);
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
<RefSubLayout.EducationRow> <RefSubLayout.Details>
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
await supabase.auth.signIn(
phone: '+13334445555',
password: 'example-password',
);
await supabase.auth.signInWithPassword(
phone: '+13334445555',
password: 'example-password',
);
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
<RefSubLayout.EducationRow> <RefSubLayout.Details>
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
final res = await supabase.auth.signIn(phone: phone);
await supabase.auth.signInWithOtp(
phone: phone,
);
// After receiving an SMS with an OTP.
await supabase.auth.verifyOTP(
type: OtpType.sms,
token: token,
phone: phone,
);
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
<RefSubLayout.EducationRow> <RefSubLayout.Details>
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
await supabase.auth.api.resetPasswordForEmail(
email,
options:
AuthOptions(redirectTo: 'io.supabase.flutter://reset-callback/'),
);
await supabase.auth.resetPasswordForEmail(
email,
redirectTo: kIsWeb ? null : 'io.supabase.flutter://reset-callback/',
);
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
<RefSubLayout.EducationRow> <RefSubLayout.Details>
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
final session = supabase.auth.session();
final Session? session = supabase.auth.currentSession;
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
<RefSubLayout.EducationRow> <RefSubLayout.Details>
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
final user = supabase.auth.user();
final User? user = supabase.auth.currentUser;
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
<RefSubLayout.EducationRow> <RefSubLayout.Details>
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
await supabase.auth.update(
UserAttributes(data: {'hello': 'world'})
);
await supabase.updateUser(
UserAttributes(
data: { 'hello': 'world' },
),
);
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
.insert() / .upsert() / .update() / .delete() no longer return rows by default. Previously, these methods return inserted/updated/deleted rows by default (which caused some confusion), and you can opt to not return it by specifying returning: 'minimal'. Now the default behavior is to not return rows. To return inserted/updated/deleted rows, add a .select() call at the end.
Also, calling .execute() at the end of the query was a requirement in v0, but deprecated in v1.
<RefSubLayout.EducationRow> <RefSubLayout.Details>
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
await supabase
.from('my_table')
.insert(data, returning: ReturningOption.minimal)
.execute();
await supabase.from('my_table').insert(data);
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
<RefSubLayout.EducationRow> <RefSubLayout.Details>
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
final res = await supabase
.from('my_table')
.insert(data)
.execute();
final insertedData = await supabase.from('my_table').insert(data).select();
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
<RefSubLayout.EducationRow> <RefSubLayout.Details>
.stream() no longer needs the .execute() at the end. Also, filtering by eq is a lot easier now. primaryKey is now a named parameter to make it more obvious what to pass.
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
supabase.from('my_table:id=eq.120')
.stream(['id'])
.listen();
supabase.from('my_table')
.stream(primaryKey: ['id'])
.eq('id', '120')
.listen();
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
<RefSubLayout.EducationRow> <RefSubLayout.Details>
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
final subscription = supabase
.from('countries')
.on(SupabaseEventTypes.all, (payload) {
// Handle realtime payload
})
.subscribe();
final channel = supabase.channel('*');
channel.on(
RealtimeListenTypes.postgresChanges,
ChannelFilter(event: '*', schema: '*'),
(payload, [ref]) {
// Handle realtime payload
},
).subscribe();
</RefSubLayout.Examples> </RefSubLayout.EducationRow>
<RefSubLayout.EducationRow> <RefSubLayout.Details>
</RefSubLayout.Details> <RefSubLayout.Examples>
<Tabs scrollable size="small" type="underlined" defaultActiveId="0.x" queryGroup="version"
<TabPanel id="0.x" label="Before">
supabase.removeSubscription(subscription);
await supabase.removeChannel(channel);
</RefSubLayout.Examples> </RefSubLayout.EducationRow>