implemented RemoteConfigProvider

This commit is contained in:
Vadim 2023-10-31 18:18:23 +01:00
parent 822ba8c7f8
commit d7162c7529
5 changed files with 211 additions and 10 deletions

View file

@ -44,10 +44,10 @@ class ApplicationWrapper extends StatelessWidget {
hapticsService: const HapticsService(), hapticsService: const HapticsService(),
lightSensorService: const LightSensorService(LocalPlatform()), lightSensorService: const LightSensorService(LocalPlatform()),
permissionsService: const PermissionsService(), permissionsService: const PermissionsService(),
remoteConfigService: const RemoteConfigService(),
userPreferencesService: userPreferencesService, userPreferencesService: userPreferencesService,
volumeEventsService: const VolumeEventsService(LocalPlatform()), volumeEventsService: const VolumeEventsService(LocalPlatform()),
child: RemoteConfig( child: RemoteConfigProvider(
remoteConfigService: const RemoteConfigService(),
child: EquipmentProfileProvider( child: EquipmentProfileProvider(
storageService: iapService, storageService: iapService,
child: FilmsProvider( child: FilmsProvider(

View file

@ -34,9 +34,48 @@ class RemoteConfigService {
} }
} }
dynamic getValue(Feature feature) => FirebaseRemoteConfig.instance.getValue(feature.name).toValue(feature);
Map<Feature, dynamic> getAll() {
final Map<Feature, dynamic> 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<Set<Feature>> onConfigUpdated() => FirebaseRemoteConfig.instance.onConfigUpdated.asyncMap(
(event) async {
await FirebaseRemoteConfig.instance.activate();
final Set<Feature> 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); bool isEnabled(Feature feature) => FirebaseRemoteConfig.instance.getBool(feature.name);
void _logError(dynamic throwable, {StackTrace? stackTrace}) { void _logError(dynamic throwable, {StackTrace? stackTrace}) {
FirebaseCrashlytics.instance.recordError(throwable, stackTrace); FirebaseCrashlytics.instance.recordError(throwable, stackTrace);
} }
} }
extension on RemoteConfigValue {
dynamic toValue(Feature feature) {
switch (feature) {
case Feature.unlockProFeaturesText:
return asBool();
}
}
}

View file

@ -1,17 +1,78 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/feature.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 { class RemoteConfigProvider extends StatefulWidget {
const RemoteConfig({ final RemoteConfigService remoteConfigService;
final Widget child;
const RemoteConfigProvider({
required this.remoteConfigService,
required this.child,
super.key, super.key,
required super.child,
}); });
@override
State<RemoteConfigProvider> createState() => RemoteConfigProviderState();
}
class RemoteConfigProviderState extends State<RemoteConfigProvider> {
late final Map<Feature, dynamic> _config = widget.remoteConfigService.getAll();
late final StreamSubscription<Set<Feature>> _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<Feature> updatedFeatures) {
for (final feature in updatedFeatures) {
_config[feature] = widget.remoteConfigService.getValue(feature);
}
setState(() {});
}
}
class RemoteConfig extends InheritedModel<Feature> {
final Map<Feature, dynamic> _config;
const RemoteConfig({
super.key,
required Map<Feature, dynamic> config,
required super.child,
}) : _config = config;
static bool isEnabled(BuildContext context, Feature feature) { static bool isEnabled(BuildContext context, Feature feature) {
return ServicesProvider.of(context).remoteConfigService.isEnabled(feature); return InheritedModel.inheritFrom<RemoteConfig>(context)!._config[feature] as bool;
} }
@override @override
bool updateShouldNotify(RemoteConfig oldWidget) => true; bool updateShouldNotify(RemoteConfig oldWidget) => true;
@override
bool updateShouldNotifyDependent(RemoteConfig oldWidget, Set<Feature> features) {
for (final feature in features) {
if (oldWidget._config[feature] != _config[feature]) {
return true;
}
}
return false;
}
} }

View file

@ -4,7 +4,6 @@ import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart'; import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart'; import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/permissions_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/shared_prefs_service.dart';
import 'package:lightmeter/data/volume_events_service.dart'; import 'package:lightmeter/data/volume_events_service.dart';
import 'package:lightmeter/environment.dart'; import 'package:lightmeter/environment.dart';
@ -17,7 +16,6 @@ class ServicesProvider extends InheritedWidget {
final HapticsService hapticsService; final HapticsService hapticsService;
final LightSensorService lightSensorService; final LightSensorService lightSensorService;
final PermissionsService permissionsService; final PermissionsService permissionsService;
final RemoteConfigService remoteConfigService;
final UserPreferencesService userPreferencesService; final UserPreferencesService userPreferencesService;
final VolumeEventsService volumeEventsService; final VolumeEventsService volumeEventsService;
@ -28,7 +26,6 @@ class ServicesProvider extends InheritedWidget {
required this.hapticsService, required this.hapticsService,
required this.lightSensorService, required this.lightSensorService,
required this.permissionsService, required this.permissionsService,
required this.remoteConfigService,
required this.userPreferencesService, required this.userPreferencesService,
required this.volumeEventsService, required this.volumeEventsService,
required super.child, required super.child,

View file

@ -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<void> 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<Set<Feature>> remoteConfigUpdateController = StreamController<Set<Feature>>();
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)}",
),
),
),
);
}
}