11import 'dart:async' ;
22import 'dart:developer' as dev;
3+ import 'dart:io' ;
34
45import 'package:async/async.dart' ;
56import 'package:flutter/foundation.dart' ;
7+ import 'package:flutter/widgets.dart' ;
68import 'package:http/http.dart' ;
79import 'package:logging/logging.dart' ;
810import 'package:supabase/supabase.dart' ;
@@ -32,7 +34,7 @@ final _log = Logger('supabase.supabase_flutter');
3234/// See also:
3335///
3436/// * [SupabaseAuth]
35- class Supabase {
37+ class Supabase with WidgetsBindingObserver {
3638 /// Gets the current supabase instance.
3739 ///
3840 /// An [AssertionError] is thrown if supabase isn't initialized yet.
@@ -126,15 +128,18 @@ class Supabase {
126128 accessToken: accessToken,
127129 );
128130
129- _instance._supabaseAuth = SupabaseAuth ();
130- await _instance._supabaseAuth.initialize (options: authOptions);
131+ if (accessToken == null ) {
132+ final supabaseAuth = SupabaseAuth ();
133+ _instance._supabaseAuth = supabaseAuth;
134+ await supabaseAuth.initialize (options: authOptions);
131135
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- );
136+ // Wrap `recoverSession()` in a `CancelableOperation` so that it can be canceled in dispose
137+ // if still in progress
138+ _instance._restoreSessionCancellableOperation =
139+ CancelableOperation .fromFuture (
140+ supabaseAuth.recoverSession (),
141+ );
142+ }
138143
139144 _log.info ('***** Supabase init completed *****' );
140145
@@ -144,28 +149,33 @@ class Supabase {
144149 Supabase ._();
145150 static final Supabase _instance = Supabase ._();
146151
152+ static WidgetsBinding ? get _widgetsBindingInstance => WidgetsBinding .instance;
153+
147154 bool _initialized = false ;
148155
149156 /// The supabase client for this instance
150157 ///
151158 /// Throws an error if [Supabase.initialize] was not called.
152159 late SupabaseClient client;
153160
154- late SupabaseAuth _supabaseAuth;
161+ SupabaseAuth ? _supabaseAuth;
155162
156163 bool _debugEnable = false ;
157164
158165 /// Wraps the `recoverSession()` call so that it can be terminated when `dispose()` is called
159166 late CancelableOperation _restoreSessionCancellableOperation;
160167
168+ CancelableOperation <void >? _realtimeReconnectOperation;
169+
161170 StreamSubscription ? _logSubscription;
162171
163172 /// Dispose the instance to free up resources.
164173 Future <void > dispose () async {
165174 await _restoreSessionCancellableOperation.cancel ();
166175 _logSubscription? .cancel ();
167176 client.dispose ();
168- _instance._supabaseAuth.dispose ();
177+ _instance._supabaseAuth? .dispose ();
178+ _widgetsBindingInstance? .removeObserver (this );
169179 _initialized = false ;
170180 }
171181
@@ -195,6 +205,76 @@ class Supabase {
195205 authOptions: authOptions,
196206 accessToken: accessToken,
197207 );
208+ _widgetsBindingInstance? .addObserver (this );
198209 _initialized = true ;
199210 }
211+
212+ @override
213+ void didChangeAppLifecycleState (AppLifecycleState state) {
214+ switch (state) {
215+ case AppLifecycleState .resumed:
216+ onResumed ();
217+ case AppLifecycleState .detached:
218+ case AppLifecycleState .paused:
219+ _realtimeReconnectOperation? .cancel ();
220+ Supabase .instance.client.realtime.disconnect ();
221+ default :
222+ }
223+ }
224+
225+ Future <void > onResumed () async {
226+ final realtime = Supabase .instance.client.realtime;
227+ if (realtime.channels.isNotEmpty) {
228+ if (realtime.connState == SocketStates .disconnecting) {
229+ // If the socket is still disconnecting from e.g.
230+ // [AppLifecycleState.paused] we should wait for it to finish before
231+ // reconnecting.
232+
233+ bool cancel = false ;
234+ final connectFuture = realtime.conn! .sink.done.then (
235+ (_) async {
236+ // Make this connect cancelable so that it does not connect if the
237+ // disconnect took so long that the app is already in background
238+ // again.
239+
240+ if (! cancel) {
241+ // ignore: invalid_use_of_internal_member
242+ await realtime.connect ();
243+ for (final channel in realtime.channels) {
244+ // ignore: invalid_use_of_internal_member
245+ if (channel.isJoined) {
246+ // ignore: invalid_use_of_internal_member
247+ channel.forceRejoin ();
248+ }
249+ }
250+ }
251+ },
252+ onError: (error) {},
253+ );
254+ _realtimeReconnectOperation = CancelableOperation .fromFuture (
255+ connectFuture,
256+ onCancel: () => cancel = true ,
257+ );
258+ } else if (! realtime.isConnected) {
259+ // Reconnect if the socket is currently not connected.
260+ // When coming from [AppLifecycleState.paused] this should be the case,
261+ // but when coming from [AppLifecycleState.inactive] no disconnect
262+ // happened and therefore connection should still be intanct and we
263+ // should not reconnect.
264+
265+ // ignore: invalid_use_of_internal_member
266+ await realtime.connect ();
267+ for (final channel in realtime.channels) {
268+ // Only rejoin channels that think they are still joined and not
269+ // which were manually unsubscribed by the user while in background
270+
271+ // ignore: invalid_use_of_internal_member
272+ if (channel.isJoined) {
273+ // ignore: invalid_use_of_internal_member
274+ channel.forceRejoin ();
275+ }
276+ }
277+ }
278+ }
279+ }
200280}
0 commit comments