Skip to content

Commit

Permalink
feat: web based applications will now flush events before closing to …
Browse files Browse the repository at this point in the history
…ensure events are sent (#129)

**Requirements**

- [x] I have added test coverage for new or changed functionality

- [x] I have followed the repository's [pull request submission
guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests)

- [x] I have validated my changes against all supported platform
versions

**Describe the solution you've provided**

Created LDAppLifecycleListener and configured to use conditional
imports. In the web case, uses an LDAppLifecycleListener that uses html
document visibility. In the non-web case, uses the existing Flutter
AppLifecycleListener. Note that the web based LDAppLifecycleListener
only emits resume and hidden, no other events.
  • Loading branch information
tanderson-ld committed Feb 15, 2024
1 parent f8244ab commit c1e2828
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 11 deletions.
22 changes: 17 additions & 5 deletions packages/flutter_client_sdk/lib/src/connection_manager.dart
Expand Up @@ -40,6 +40,8 @@ abstract interface class ConnectionDestination {
void setNetworkAvailability(bool available);

void setEventSendingEnabled(bool enabled, {bool flush = true});

void flush();
}

/// Basic adapter that turns an LDCommonClient into a ConnectionDestination.
Expand All @@ -62,6 +64,11 @@ final class DartClientAdapter implements ConnectionDestination {
void setEventSendingEnabled(bool enabled, {bool flush = true}) {
_client.setEventSendingEnabled(enabled, flush: flush);
}

@override
void flush() {
_client.flush();
}
}

final class ConnectionManagerConfig {
Expand Down Expand Up @@ -160,24 +167,29 @@ final class ConnectionManager {
return;
}

/// Currently the foreground mode will always be whatever the last active
/// connection mode was.
// Currently the foreground mode will always be whatever the last active
// connection mode was.
_destination.setMode(_currentConnectionMode);
_destination.setEventSendingEnabled(true);
}

void _setBackgroundAvailableMode() {
// flush on backgrounding as application may be killed and we don't want to lose events.
_destination.flush();

if (!_config.runInBackground) {
// TODO: Can we support the backgroundDisabled data source status?
// TODO: Is it acceptable for the data source status and `offline` to
// report an `offline` status?
_destination.setMode(ConnectionMode.offline);
_destination.setEventSendingEnabled(false);

// no need to flush here, we just did up above
_destination.setEventSendingEnabled(false, flush: false);
return;
}

/// If connections in the background are allowed, then use the same mode
/// as is configured for the foreground.
// If connections in the background are allowed, then use the same mode
// as is configured for the foreground.
_setForegroundAvailableMode();
}

Expand Down
13 changes: 7 additions & 6 deletions packages/flutter_client_sdk/lib/src/flutter_state_detector.dart
Expand Up @@ -2,9 +2,11 @@ import 'dart:async';

import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';

import 'connection_manager.dart';
import 'lifecycle/stub_lifecycle_listener.dart'
if (dart.library.io) 'lifecycle/io_lifecycle_listener.dart'
if (dart.library.html) 'lifecycle/js_lifecycle_listener.dart';

/// This class detects the application and network state for flutter.
final class FlutterStateDetector implements StateDetector {
Expand All @@ -20,7 +22,7 @@ final class FlutterStateDetector implements StateDetector {
@override
Stream<NetworkState> get networkState => _networkStateController.stream;

late final AppLifecycleListener _lifecycleListener;
late final LDAppLifecycleListener _lifecycleListener;
late final StreamSubscription<ConnectivityResult> _connectivitySubscription;

FlutterStateDetector() {
Expand All @@ -29,9 +31,8 @@ final class FlutterStateDetector implements StateDetector {
_handleApplicationLifecycle(initialState);
}

_lifecycleListener = AppLifecycleListener(
onStateChange: (state) => _handleApplicationLifecycle(state),
);
_lifecycleListener = LDAppLifecycleListener();
_lifecycleListener.stream.listen(_handleApplicationLifecycle);

Connectivity().checkConnectivity().then(_setConnectivity);

Expand Down Expand Up @@ -100,7 +101,7 @@ final class FlutterStateDetector implements StateDetector {

@override
void dispose() {
_lifecycleListener.dispose();
_lifecycleListener.close();
_applicationStateController.close();
_networkStateController.close();
_connectivitySubscription.cancel();
Expand Down
@@ -0,0 +1,27 @@
import 'dart:async';

import 'package:flutter/widgets.dart';

/// Lifecycle listener that uses the Flutter [AppLifecycleListener].
/// Unfortunately, the [AppLifecycleListener] does not support web very well at
/// the moment, so the [LDAppLifecycleListener] was created.
class LDAppLifecycleListener {
late final StreamController<AppLifecycleState> _streamController;
AppLifecycleListener? _underlyingListener;

LDAppLifecycleListener() {
_streamController = StreamController.broadcast(onListen: () {
_underlyingListener = AppLifecycleListener(
onStateChange: (state) => _streamController.add(state));
}, onCancel: () {
_underlyingListener?.dispose();
_underlyingListener = null;
});
}

Stream<AppLifecycleState> get stream => _streamController.stream;

void close() {
_streamController.close();
}
}
@@ -0,0 +1,33 @@
import 'dart:async';
import 'dart:html' as html;

import 'package:flutter/widgets.dart';

/// Lifecycle listener that uses the underlying visibility of the html web
/// document to emit events.
class LDAppLifecycleListener {
late final StreamController<AppLifecycleState> _streamController;

LDAppLifecycleListener() {
_streamController = StreamController.broadcast();

void listenerFunc(event) =>
_streamController.add(html.document.hidden == true
? AppLifecycleState.hidden
: AppLifecycleState.resumed);

_streamController.onListen = () {
html.document.addEventListener('visibilitychange', listenerFunc);
};

_streamController.onCancel = () {
html.document.removeEventListener('visibilitychange', listenerFunc);
};
}

Stream<AppLifecycleState> get stream => _streamController.stream;

void close() {
_streamController.close();
}
}
@@ -0,0 +1,10 @@
import 'package:flutter/widgets.dart';

class LDAppLifecycleListener {
Stream<AppLifecycleState> get stream =>
throw Exception('Stub implementation');

void close() {
throw Exception('Stub implementation');
}
}
Expand Up @@ -148,6 +148,7 @@ void main() {
// Wait for the state to propagate.
await mockDetector.applicationState.first;

verify(() => destination.flush());
verify(() => destination.setMode(ConnectionMode.streaming));
connectionManager.dispose();
});
Expand Down

0 comments on commit c1e2828

Please sign in to comment.