1
1
import 'dart:async' ;
2
- import 'dart:developer' as dev;
3
2
4
3
import 'package:async/async.dart' ;
5
4
import 'package:flutter/foundation.dart' ;
5
+ import 'package:flutter/widgets.dart' ;
6
6
import 'package:http/http.dart' ;
7
7
import 'package:logging/logging.dart' ;
8
8
import 'package:supabase/supabase.dart' ;
@@ -32,7 +32,7 @@ final _log = Logger('supabase.supabase_flutter');
32
32
/// See also:
33
33
///
34
34
/// * [SupabaseAuth]
35
- class Supabase {
35
+ class Supabase with WidgetsBindingObserver {
36
36
/// Gets the current supabase instance.
37
37
///
38
38
/// An [AssertionError] is thrown if supabase isn't initialized yet.
@@ -126,15 +126,18 @@ class Supabase {
126
126
accessToken: accessToken,
127
127
);
128
128
129
- _instance._supabaseAuth = SupabaseAuth ();
130
- await _instance._supabaseAuth.initialize (options: authOptions);
129
+ if (accessToken == null ) {
130
+ final supabaseAuth = SupabaseAuth ();
131
+ _instance._supabaseAuth = supabaseAuth;
132
+ await supabaseAuth.initialize (options: authOptions);
131
133
132
- // Wrap `recoverSession()` in a `CancelableOperation` so that it can be canceled in dispose
133
- // if still in progress
134
- _instance._restoreSessionCancellableOperation =
135
- CancelableOperation .fromFuture (
136
- _instance._supabaseAuth.recoverSession (),
137
- );
134
+ // Wrap `recoverSession()` in a `CancelableOperation` so that it can be canceled in dispose
135
+ // if still in progress
136
+ _instance._restoreSessionCancellableOperation =
137
+ CancelableOperation .fromFuture (
138
+ supabaseAuth.recoverSession (),
139
+ );
140
+ }
138
141
139
142
_log.info ('***** Supabase init completed *****' );
140
143
@@ -144,28 +147,33 @@ class Supabase {
144
147
Supabase ._();
145
148
static final Supabase _instance = Supabase ._();
146
149
150
+ static WidgetsBinding ? get _widgetsBindingInstance => WidgetsBinding .instance;
151
+
147
152
bool _initialized = false ;
148
153
149
154
/// The supabase client for this instance
150
155
///
151
156
/// Throws an error if [Supabase.initialize] was not called.
152
157
late SupabaseClient client;
153
158
154
- late SupabaseAuth _supabaseAuth;
159
+ SupabaseAuth ? _supabaseAuth;
155
160
156
161
bool _debugEnable = false ;
157
162
158
163
/// Wraps the `recoverSession()` call so that it can be terminated when `dispose()` is called
159
164
late CancelableOperation _restoreSessionCancellableOperation;
160
165
166
+ CancelableOperation <void >? _realtimeReconnectOperation;
167
+
161
168
StreamSubscription ? _logSubscription;
162
169
163
170
/// Dispose the instance to free up resources.
164
171
Future <void > dispose () async {
165
172
await _restoreSessionCancellableOperation.cancel ();
166
173
_logSubscription? .cancel ();
167
174
client.dispose ();
168
- _instance._supabaseAuth.dispose ();
175
+ _instance._supabaseAuth? .dispose ();
176
+ _widgetsBindingInstance? .removeObserver (this );
169
177
_initialized = false ;
170
178
}
171
179
@@ -195,6 +203,76 @@ class Supabase {
195
203
authOptions: authOptions,
196
204
accessToken: accessToken,
197
205
);
206
+ _widgetsBindingInstance? .addObserver (this );
198
207
_initialized = true ;
199
208
}
209
+
210
+ @override
211
+ void didChangeAppLifecycleState (AppLifecycleState state) {
212
+ switch (state) {
213
+ case AppLifecycleState .resumed:
214
+ onResumed ();
215
+ case AppLifecycleState .detached:
216
+ case AppLifecycleState .paused:
217
+ _realtimeReconnectOperation? .cancel ();
218
+ Supabase .instance.client.realtime.disconnect ();
219
+ default :
220
+ }
221
+ }
222
+
223
+ Future <void > onResumed () async {
224
+ final realtime = Supabase .instance.client.realtime;
225
+ if (realtime.channels.isNotEmpty) {
226
+ if (realtime.connState == SocketStates .disconnecting) {
227
+ // If the socket is still disconnecting from e.g.
228
+ // [AppLifecycleState.paused] we should wait for it to finish before
229
+ // reconnecting.
230
+
231
+ bool cancel = false ;
232
+ final connectFuture = realtime.conn! .sink.done.then (
233
+ (_) async {
234
+ // Make this connect cancelable so that it does not connect if the
235
+ // disconnect took so long that the app is already in background
236
+ // again.
237
+
238
+ if (! cancel) {
239
+ // ignore: invalid_use_of_internal_member
240
+ await realtime.connect ();
241
+ for (final channel in realtime.channels) {
242
+ // ignore: invalid_use_of_internal_member
243
+ if (channel.isJoined) {
244
+ // ignore: invalid_use_of_internal_member
245
+ channel.forceRejoin ();
246
+ }
247
+ }
248
+ }
249
+ },
250
+ onError: (error) {},
251
+ );
252
+ _realtimeReconnectOperation = CancelableOperation .fromFuture (
253
+ connectFuture,
254
+ onCancel: () => cancel = true ,
255
+ );
256
+ } else if (! realtime.isConnected) {
257
+ // Reconnect if the socket is currently not connected.
258
+ // When coming from [AppLifecycleState.paused] this should be the case,
259
+ // but when coming from [AppLifecycleState.inactive] no disconnect
260
+ // happened and therefore connection should still be intanct and we
261
+ // should not reconnect.
262
+
263
+ // ignore: invalid_use_of_internal_member
264
+ await realtime.connect ();
265
+ for (final channel in realtime.channels) {
266
+ // Only rejoin channels that think they are still joined and not
267
+ // which were manually unsubscribed by the user while in background
268
+
269
+ // ignore: invalid_use_of_internal_member
270
+ if (channel.isJoined) {
271
+ // ignore: invalid_use_of_internal_member
272
+ channel.forceRejoin ();
273
+ }
274
+ }
275
+ }
276
+ }
277
+ }
200
278
}
0 commit comments