From d7162c752962e6a231f5273ba8636c9ca1f10351 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Tue, 31 Oct 2023 18:18:23 +0100 Subject: [PATCH] implemented `RemoteConfigProvider` --- lib/application_wrapper.dart | 4 +- lib/data/remote_config_service.dart | 39 +++++++ lib/providers/remote_config_provider.dart | 71 +++++++++++- lib/providers/services_provider.dart | 3 - .../remote_config_provider_test.dart | 104 ++++++++++++++++++ 5 files changed, 211 insertions(+), 10 deletions(-) create mode 100644 test/providers/remote_config_provider_test.dart diff --git a/lib/application_wrapper.dart b/lib/application_wrapper.dart index d0772ee..d28fbcf 100644 --- a/lib/application_wrapper.dart +++ b/lib/application_wrapper.dart @@ -44,10 +44,10 @@ class ApplicationWrapper extends StatelessWidget { hapticsService: const HapticsService(), lightSensorService: const LightSensorService(LocalPlatform()), permissionsService: const PermissionsService(), - remoteConfigService: const RemoteConfigService(), userPreferencesService: userPreferencesService, volumeEventsService: const VolumeEventsService(LocalPlatform()), - child: RemoteConfig( + child: RemoteConfigProvider( + remoteConfigService: const RemoteConfigService(), child: EquipmentProfileProvider( storageService: iapService, child: FilmsProvider( diff --git a/lib/data/remote_config_service.dart b/lib/data/remote_config_service.dart index 3a247b6..66fa1a0 100644 --- a/lib/data/remote_config_service.dart +++ b/lib/data/remote_config_service.dart @@ -34,9 +34,48 @@ class RemoteConfigService { } } + dynamic getValue(Feature feature) => FirebaseRemoteConfig.instance.getValue(feature.name).toValue(feature); + + Map getAll() { + final Map result = {}; + for (final value in FirebaseRemoteConfig.instance.getAll().entries) { + try { + final feature = Feature.values.firstWhere((f) => f.name == value.key); + result[feature] = MapEntry(feature, value.value.toValue(feature)); + } catch (e) { + log(e.toString()); + } + } + return result; + } + + Stream> onConfigUpdated() => FirebaseRemoteConfig.instance.onConfigUpdated.asyncMap( + (event) async { + await FirebaseRemoteConfig.instance.activate(); + final Set updatedFeatures = {}; + for (final key in event.updatedKeys) { + try { + updatedFeatures.add(Feature.values.firstWhere((element) => element.name == key)); + } catch (e) { + log(e.toString()); + } + } + return updatedFeatures; + }, + ); + bool isEnabled(Feature feature) => FirebaseRemoteConfig.instance.getBool(feature.name); void _logError(dynamic throwable, {StackTrace? stackTrace}) { FirebaseCrashlytics.instance.recordError(throwable, stackTrace); } } + +extension on RemoteConfigValue { + dynamic toValue(Feature feature) { + switch (feature) { + case Feature.unlockProFeaturesText: + return asBool(); + } + } +} diff --git a/lib/providers/remote_config_provider.dart b/lib/providers/remote_config_provider.dart index 2b05073..9736e1d 100644 --- a/lib/providers/remote_config_provider.dart +++ b/lib/providers/remote_config_provider.dart @@ -1,17 +1,78 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/feature.dart'; -import 'package:lightmeter/providers/services_provider.dart'; +import 'package:lightmeter/data/remote_config_service.dart'; -class RemoteConfig extends InheritedWidget { - const RemoteConfig({ +class RemoteConfigProvider extends StatefulWidget { + final RemoteConfigService remoteConfigService; + final Widget child; + + const RemoteConfigProvider({ + required this.remoteConfigService, + required this.child, super.key, - required super.child, }); + @override + State createState() => RemoteConfigProviderState(); +} + +class RemoteConfigProviderState extends State { + late final Map _config = widget.remoteConfigService.getAll(); + late final StreamSubscription> _updatesSubscription; + + @override + void initState() { + super.initState(); + _updatesSubscription = widget.remoteConfigService.onConfigUpdated().listen(_updateFeatures); + } + + @override + void dispose() { + _updatesSubscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return RemoteConfig( + config: _config, + child: widget.child, + ); + } + + void _updateFeatures(Set updatedFeatures) { + for (final feature in updatedFeatures) { + _config[feature] = widget.remoteConfigService.getValue(feature); + } + setState(() {}); + } +} + +class RemoteConfig extends InheritedModel { + final Map _config; + + const RemoteConfig({ + super.key, + required Map config, + required super.child, + }) : _config = config; + static bool isEnabled(BuildContext context, Feature feature) { - return ServicesProvider.of(context).remoteConfigService.isEnabled(feature); + return InheritedModel.inheritFrom(context)!._config[feature] as bool; } @override bool updateShouldNotify(RemoteConfig oldWidget) => true; + + @override + bool updateShouldNotifyDependent(RemoteConfig oldWidget, Set features) { + for (final feature in features) { + if (oldWidget._config[feature] != _config[feature]) { + return true; + } + } + return false; + } } diff --git a/lib/providers/services_provider.dart b/lib/providers/services_provider.dart index a439132..36f5674 100644 --- a/lib/providers/services_provider.dart +++ b/lib/providers/services_provider.dart @@ -4,7 +4,6 @@ import 'package:lightmeter/data/caffeine_service.dart'; import 'package:lightmeter/data/haptics_service.dart'; import 'package:lightmeter/data/light_sensor_service.dart'; import 'package:lightmeter/data/permissions_service.dart'; -import 'package:lightmeter/data/remote_config_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/data/volume_events_service.dart'; import 'package:lightmeter/environment.dart'; @@ -17,7 +16,6 @@ class ServicesProvider extends InheritedWidget { final HapticsService hapticsService; final LightSensorService lightSensorService; final PermissionsService permissionsService; - final RemoteConfigService remoteConfigService; final UserPreferencesService userPreferencesService; final VolumeEventsService volumeEventsService; @@ -28,7 +26,6 @@ class ServicesProvider extends InheritedWidget { required this.hapticsService, required this.lightSensorService, required this.permissionsService, - required this.remoteConfigService, required this.userPreferencesService, required this.volumeEventsService, required super.child, diff --git a/test/providers/remote_config_provider_test.dart b/test/providers/remote_config_provider_test.dart new file mode 100644 index 0000000..aa6815d --- /dev/null +++ b/test/providers/remote_config_provider_test.dart @@ -0,0 +1,104 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/data/models/feature.dart'; +import 'package:lightmeter/data/remote_config_service.dart'; +import 'package:lightmeter/providers/remote_config_provider.dart'; +import 'package:mocktail/mocktail.dart'; + +class _MockRemoteConfigService extends Mock implements RemoteConfigService {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late _MockRemoteConfigService mockRemoteConfigService; + + setUpAll(() { + mockRemoteConfigService = _MockRemoteConfigService(); + }); + + setUp(() { + when(() => mockRemoteConfigService.getValue(Feature.unlockProFeaturesText)).thenReturn(false); + when(() => mockRemoteConfigService.getAll()).thenReturn({Feature.unlockProFeaturesText: false}); + }); + + tearDown(() { + reset(mockRemoteConfigService); + }); + + Future pumpTestWidget(WidgetTester tester) async { + await tester.pumpWidget( + RemoteConfigProvider( + remoteConfigService: mockRemoteConfigService, + child: const _Application(), + ), + ); + } + + testWidgets( + 'RemoteConfigProvider init', + (tester) async { + when(() => mockRemoteConfigService.onConfigUpdated()).thenAnswer((_) => const Stream.empty()); + + await pumpTestWidget(tester); + expect(find.text('unlockProFeaturesText: false'), findsOneWidget); + }, + ); + + testWidgets( + 'RemoteConfigProvider updates stream', + (tester) async { + final StreamController> remoteConfigUpdateController = StreamController>(); + when(() => mockRemoteConfigService.onConfigUpdated()).thenAnswer((_) => remoteConfigUpdateController.stream); + + await pumpTestWidget(tester); + expect(find.text('unlockProFeaturesText: false'), findsOneWidget); + + when(() => mockRemoteConfigService.getValue(Feature.unlockProFeaturesText)).thenReturn(true); + remoteConfigUpdateController.add({Feature.unlockProFeaturesText}); + await tester.pumpAndSettle(); + expect(find.text('unlockProFeaturesText: true'), findsOneWidget); + + await remoteConfigUpdateController.close(); + }, + ); + + test('RemoteConfig.updateShouldNotifyDependent', () { + const config = RemoteConfig(config: {Feature.unlockProFeaturesText: false}, child: SizedBox()); + expect( + config.updateShouldNotifyDependent(config, {}), + false, + ); + expect( + config.updateShouldNotifyDependent( + const RemoteConfig(config: {Feature.unlockProFeaturesText: false}, child: SizedBox()), + {Feature.unlockProFeaturesText}, + ), + false, + ); + expect( + config.updateShouldNotifyDependent( + const RemoteConfig(config: {Feature.unlockProFeaturesText: true}, child: SizedBox()), + {Feature.unlockProFeaturesText}, + ), + true, + ); + }); +} + +class _Application extends StatelessWidget { + const _Application(); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: Center( + child: Text( + "${Feature.unlockProFeaturesText.name}: ${RemoteConfig.isEnabled(context, Feature.unlockProFeaturesText)}", + ), + ), + ), + ); + } +}