Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Flags based on OpenFeature Specs #70

Merged
merged 22 commits into from Jun 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6a72432
:sparkles: added `core`
sarbagyastha Jun 6, 2022
2a9bde4
:sparkles: added `evaluation_context`
sarbagyastha Jun 6, 2022
0a6d3fd
:sparkles: added `exceptions`
sarbagyastha Jun 6, 2022
b2cf092
:sparkles: added `flag_evaluation`
sarbagyastha Jun 6, 2022
7010638
:sparkles: added `hook`
sarbagyastha Jun 6, 2022
4f106ef
:sparkles: added `provider`
sarbagyastha Jun 6, 2022
014816b
:sparkles: added `OpenFeatureClient`
sarbagyastha Jun 6, 2022
b63b71a
:sparkles: added `OpenFeature` class
sarbagyastha Jun 6, 2022
98b7018
:sparkles: added `JsonFeatureProvider`
sarbagyastha Jun 6, 2022
86785e9
:sparkles: exported open-feature
sarbagyastha Jun 6, 2022
f7f9ad2
:sparkles: added `FeatureBuilder`
sarbagyastha Jun 6, 2022
48da7ad
:sparkles: added `EvaluationEngine`
sarbagyastha Jun 6, 2022
22be6ac
:bento: added `flags.json`
sarbagyastha Jun 6, 2022
1509efb
:sparkles: added `FeatureBuilder`
sarbagyastha Jun 6, 2022
3dc98c7
:sparkles: added `FeatureScope`
sarbagyastha Jun 6, 2022
09338d0
:sparkles: update app bar color based on feature flag
sarbagyastha Jun 6, 2022
f74443c
:bug: handled erronous cases for flag resolution
sarbagyastha Jun 7, 2022
721e4ed
:white_check_mark: fixed failing tests
sarbagyastha Jun 7, 2022
5f0c80d
:bookmark: bumped version & updated changelog
sarbagyastha Jun 7, 2022
74b4763
:white_check_mark: added tests for feature builder
sarbagyastha Jun 7, 2022
4d979c0
:white_check_mark: added more test cases
sarbagyastha Jun 7, 2022
9a746d5
:heavy_minus_sign: ignored coverage for open feature files
sarbagyastha Jun 7, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,7 @@
# [1.3.1] - Jun 7, 2022
- Added Feature Flag Management based on OpenFeature specs.
- Added `FeatureBuilder` and `FeatureScope`.

# [1.3.0] - May 12, 2022
- Added `BridgeGatewayProvider`
- Fixed issue with content-type being appended with charset.
Expand Down
53 changes: 53 additions & 0 deletions example/assets/flags.json
@@ -0,0 +1,53 @@
{
"newTitle": {
"state": "disabled"
},
"color": {
"returnType": "number",
"variants": {
"red": 4294901760,
"green": 4278255360,
"blue": 4278190335,
"purple": 4285140397
},
"defaultVariant": "red",
"state": "enabled"
},
"exampleFeatures": {
"returnType": "string",
"variants": {
"query": "firebase,graphql",
"restful": "graphql,rest",
"traditional": "rest",
"all": "firebase,graphql,rest"
},
"defaultVariant": "query",
"state": "enabled",
"rules": [
{
"action": {
"variant": "restful"
},
"conditions": [
{
"context": "platform",
"op": "equals",
"value": "iOS"
}
]
},
{
"action": {
"variant": "all"
},
"conditions": [
{
"context": "platform",
"op": "equals",
"value": "android"
}
]
}
]
}
}
10 changes: 10 additions & 0 deletions example/lib/asset_feature_provider.dart
@@ -0,0 +1,10 @@
import 'package:clean_framework/clean_framework_defaults.dart';
import 'package:flutter/services.dart';

class AssetFeatureProvider extends JsonFeatureProvider {
Future<void> load(String key) async {
final rawFlags = await rootBundle.loadString(key);

feed(OpenFeatureFlags.fromJson(rawFlags));
}
}
115 changes: 91 additions & 24 deletions example/lib/home_page.dart
@@ -1,37 +1,104 @@
import 'package:clean_framework/clean_framework.dart';
import 'package:clean_framework_example/routes.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
const HomePage() : super(key: const Key('HomePage'));

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Example Features'),
),
body: ListView(
padding: EdgeInsets.symmetric(vertical: 16),
children: [
ListTile(
title: Text('Firebase'),
leading: Icon(Icons.local_fire_department_sharp),
onTap: () => router.to(Routes.lastLogin),
return FeatureBuilder<int>(
flagKey: 'color',
valueType: FlagValueType.number,
defaultValue: 0xFF0000FF,
builder: (context, colorValue) {
return Scaffold(
appBar: AppBar(
title: Text('Example Features'),
backgroundColor: Color(colorValue),
),
const Divider(),
ListTile(
title: Text('GraphQL'),
leading: Icon(Icons.graphic_eq),
onTap: () => router.to(Routes.countries),
),
const Divider(),
ListTile(
title: Text('Rest API'),
leading: Icon(Icons.sync_alt),
onTap: () => router.to(Routes.randomCat),
body: FeatureBuilder<String>(
flagKey: 'exampleFeatures',
valueType: FlagValueType.string,
defaultValue: 'rest,firebase,graphql',
evaluationContext: EvaluationContext(
{'platform': defaultTargetPlatform.name},
),
builder: (context, value) {
final enabledFeatures = value.split(',');

return _ExampleFeature(enabledFeatures: enabledFeatures);
},
),
],
),
);
},
);
}
}

class _ExampleFeature extends StatelessWidget {
const _ExampleFeature({required this.enabledFeatures});

final List<String> enabledFeatures;

@override
Widget build(BuildContext context) {
return ListView(
padding: EdgeInsets.symmetric(vertical: 16),
children: [
_List(
enabled: enabledFeatures.contains('firebase'),
title: 'Firebase',
iconData: Icons.local_fire_department_sharp,
route: Routes.lastLogin,
),
_List(
enabled: enabledFeatures.contains('graphql'),
title: 'GraphQL',
iconData: Icons.graphic_eq,
route: Routes.countries,
),
_List(
enabled: enabledFeatures.contains('rest'),
title: 'Rest API',
iconData: Icons.sync_alt,
route: Routes.randomCat,
),
],
);
}
}

class _List extends StatelessWidget {
const _List({
required this.enabled,
required this.title,
required this.iconData,
required this.route,
});

final bool enabled;
final String title;
final IconData iconData;
final Routes route;

@override
Widget build(BuildContext context) {
return AnimatedSize(
duration: const Duration(seconds: 2),
child: enabled
? Column(
children: [
ListTile(
title: Text(title),
leading: Icon(iconData),
onTap: () => router.to(route),
),
Divider(),
],
)
: const SizedBox.shrink(),
);
}
}
44 changes: 27 additions & 17 deletions example/lib/main.dart
@@ -1,33 +1,43 @@
import 'dart:developer';

import 'package:clean_framework/clean_framework.dart';
import 'package:clean_framework_example/asset_feature_provider.dart';
import 'package:clean_framework_example/providers.dart';
import 'package:clean_framework_example/routes.dart';
import 'package:flutter/material.dart';

void main() {
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
loadProviders();

runApp(ExampleApp());
}

class ExampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppProvidersContainer(
providersContext: providersContext,
onBuild: (context, _) {
providersContext().read(featureStatesProvider.featuresMap).load({
'features': [
{'name': 'last_login', 'state': 'ACTIVE'},
]
});
return FeatureScope<AssetFeatureProvider>(
register: () => AssetFeatureProvider(),
loader: (featureProvider) async {
// To demonstrate the lazy update triggered by change in feature flags.
await Future.delayed(Duration(seconds: 2));
await featureProvider.load('assets/flags.json');
},
onLoaded: () {
log('Feature Flags activated.');
},
child: MaterialApp.router(
routeInformationParser: router.informationParser,
routerDelegate: router.delegate,
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: ZoomPageTransitionsBuilder(),
},
child: AppProvidersContainer(
providersContext: providersContext,
onBuild: (context, _) {},
child: MaterialApp.router(
routeInformationParser: router.informationParser,
routerDelegate: router.delegate,
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: ZoomPageTransitionsBuilder(),
},
),
),
),
),
Expand Down
7 changes: 5 additions & 2 deletions example/pubspec.yaml
Expand Up @@ -4,8 +4,8 @@ version: 1.0.0
publish_to: 'none'

environment:
sdk: '>=2.16.0 <3.0.0'
flutter: '>=1.17.0'
sdk: '>=2.17.0 <3.0.0'
flutter: '>=3.0.0'

dependencies:
flutter:
Expand All @@ -25,3 +25,6 @@ dev_dependencies:

flutter:
uses-material-design: true

assets:
- assets/flags.json
Expand Up @@ -7,6 +7,8 @@ import 'package:clean_framework_example/routes.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import '../../../home_page_test.dart';

void main() {
setupUITest(context: providersContext, router: router);

Expand Down Expand Up @@ -89,6 +91,10 @@ void main() {

uiTest(
'tapping on country tile should navigate to detail page',
parentBuilder: (child) => FeatureScope(
register: () => FakeJsonFeatureProvider(),
child: child,
),
verify: (tester) async {
router.to(Routes.countries);
await tester.pumpAndSettle();
Expand Down
74 changes: 68 additions & 6 deletions example/test/home_page_test.dart
@@ -1,4 +1,5 @@
import 'package:clean_framework/clean_framework.dart';
import 'package:clean_framework/clean_framework_defaults.dart';
import 'package:clean_framework_example/features/country/presentation/country_ui.dart';
import 'package:clean_framework_example/features/last_login/presentation/last_login_ui.dart';
import 'package:clean_framework_example/features/random_cat/presentation/random_cat_ui.dart';
Expand Down Expand Up @@ -107,12 +108,73 @@ void main() {
}

Widget buildWidget(Widget widget) {
return AppProvidersContainer(
providersContext: providersContext,
onBuild: (_, __) {},
child: MaterialApp.router(
routeInformationParser: router.informationParser,
routerDelegate: router.delegate,
return FeatureScope(
register: () => FakeJsonFeatureProvider(),
child: AppProvidersContainer(
providersContext: providersContext,
onBuild: (_, __) {},
child: MaterialApp.router(
routeInformationParser: router.informationParser,
routerDelegate: router.delegate,
),
),
);
}

class FakeJsonFeatureProvider extends JsonFeatureProvider {
FakeJsonFeatureProvider() {
feed(OpenFeatureFlags.fromJson('''{
"newTitle": {
"state": "disabled"
},
"color": {
"returnType": "number",
"variants": {
"red": 4294901760,
"green": 4278255360,
"blue": 4278190335,
"purple": 4285140397
},
"defaultVariant": "red",
"state": "enabled"
},
"exampleFeatures": {
"returnType": "string",
"variants": {
"query": "firebase,graphql",
"restful": "graphql,rest",
"traditional": "rest",
"all": "firebase,graphql,rest"
},
"defaultVariant": "query",
"state": "enabled",
"rules": [
{
"action": {
"variant": "restful"
},
"conditions": [
{
"context": "platform",
"op": "equals",
"value": "iOS"
}
]
},
{
"action": {
"variant": "all"
},
"conditions": [
{
"context": "platform",
"op": "equals",
"value": "android"
}
]
}
]
}
}'''));
}
}