Skip to content

Commit

Permalink
Merge pull request #44 from alippo-com/fix/lateinit-of-featureDataModel
Browse files Browse the repository at this point in the history
fix: late initialization error of the featureDataModel.
  • Loading branch information
DK070202 authored May 23, 2023
2 parents ddfa692 + bfdb6c4 commit 4232e91
Show file tree
Hide file tree
Showing 20 changed files with 305 additions and 168 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# 2.0.1+0
# 2.1.0+0
- Fixes [issue](https://github.com/alippo-com/GrowthBook-SDK-Flutter/issues)

## 2.0.1+0
- Fixes [issue](https://github.com/alippo-com/GrowthBook-SDK-Flutter/issues/36) caused by analyzer.

## 2.0.0+0
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ GrowthBook is an open source feature flagging and experimentation platform that

1. Add GrowthBook SDK as dependency in your pubspec.yaml file.
```yaml
growthbook_sdk_flutter: ^1.1.0
growthbook_sdk_flutter: ^latest-version
```
## Integration
Expand Down
16 changes: 6 additions & 10 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:growthbook_sdk_flutter/growthbook_sdk_flutter.dart';

void main() {
void main() async {
runApp(const MyApp());
}

Expand Down Expand Up @@ -45,7 +45,7 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
/// Initialization of controllers.
late TabController _tabController;
final userAttr = {"id": Platform.isIOS ? "foo" : "foo_bar"};
late final GrowthBookSDK gb;
GrowthBookSDK? gb;
@override
void initState() {
super.initState();
Expand All @@ -60,12 +60,15 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
hostURL: 'https://example.growthbook.io/',
attributes: userAttr,
growthBookTrackingCallBack: (exp, rst) {},
gbFeatures: {
'some-feature': GBFeature(defaultValue: true),
},
).initialize();
setState(() {});
}

Widget _getRightWidget() {
if (gb.feature('random').on!) {
if (gb?.feature('some-feature').on! ?? false) {
return TabBar(
isScrollable: true,
tabs: tabs,
Expand Down Expand Up @@ -103,13 +106,6 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
mainAxisSize: MainAxisSize.min,
children: [
Text(tabNames[i]),
ElevatedButton(
onPressed: () {
//
gb.features.forEach((key, value) {});
},
child: const Text('Press'),
)
],
),
),
Expand Down
81 changes: 51 additions & 30 deletions lib/src/Evaluator/experiment_evaluator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import 'package:growthbook_sdk_flutter/growthbook_sdk_flutter.dart';
class GBExperimentEvaluator {
/// Takes Context & Experiment & returns Experiment Result
GBExperimentResult evaluateExperiment({
static GBExperimentResult evaluateExperiment({
required GBContext context,
required GBExperiment experiment,
}) {
Expand All @@ -14,40 +14,53 @@ class GBExperimentEvaluator {
///
/// If context.enabled is false, return immediately (not in experiment, variationId 0)
if (experiment.variations.length < 2 || !context.enabled!) {
return _getExperimentResult(experiment: experiment, gbContext: context);
return _getExperimentResult(
experiment: experiment,
gbContext: context,
);
}

/// If context.forcedVariations[experiment.trackingKey] is defined,
/// return immediately (not in experiment, forced variation)
final forcedVariation = context.forcedVariation?[experiment.key];
if (forcedVariation != null) {
return _getExperimentResult(
experiment: experiment,
variationIndex: forcedVariation,
gbContext: context);
experiment: experiment,
variationIndex: forcedVariation,
gbContext: context,
);
}

/// If experiment.action is set to false, return immediately
/// (not in experiment, variationId 0)
if (experiment.deactivated) {
return _getExperimentResult(experiment: experiment, gbContext: context);
return _getExperimentResult(
experiment: experiment,
gbContext: context,
);
}

// Get the user hash attribute and value (context.attributes[experiment.hashAttribute || "id"])
// and if empty, return immediately (not in experiment, variationId 0)
final attributeValue =
context.attributes?[experiment.hashAttribute ?? Constant.idAttribute];
if (attributeValue == null || attributeValue.toString().isEmpty) {
return _getExperimentResult(experiment: experiment, gbContext: context);
return _getExperimentResult(
experiment: experiment,
gbContext: context,
);
}

/// If experiment.namespace is set, check if hash value is included in the
/// range and if not, return immediately (not in experiment, variationId 0)
if (experiment.namespace != null) {
var namespace = const GBUtils().getGBNameSpace(experiment.namespace!);
if (experiment.namespace != null &&
!const GBUtils().inNamespace(attributeValue, namespace!)) {
return _getExperimentResult(experiment: experiment, gbContext: context);
var namespace = GBUtils.getGBNameSpace(experiment.namespace!);
if (namespace != null &&
!GBUtils.inNamespace(attributeValue, namespace)) {
return _getExperimentResult(
experiment: experiment,
gbContext: context,
);
}
}

Expand All @@ -57,7 +70,10 @@ class GBExperimentEvaluator {
final attr = context.attributes;
if (!GBConditionEvaluator()
.evaluateCondition(attr!, experiment.condition!)) {
return _getExperimentResult(experiment: experiment, gbContext: context);
return _getExperimentResult(
experiment: experiment,
gbContext: context,
);
}
}

Expand All @@ -66,15 +82,16 @@ class GBExperimentEvaluator {
if (weights == null) {
// Default weights to an even split between all variations
experiment.weights =
const GBUtils().getEqualWeights(experiment.variations.length);
GBUtils.getEqualWeights(experiment.variations.length);
}

// Default coverage 1.
final coverage = experiment.coverage ?? 1.0;
experiment.coverage = coverage;

/// Calculate bucket ranges for the variations
/// Convert weights/coverage to ranges
final List<GBBucketRange> bucketRange = const GBUtils().getBucketRanges(
final List<GBBucketRange> bucketRange = GBUtils.getBucketRanges(
experiment.variations.length,
coverage,
experiment.weights != null
Expand All @@ -83,14 +100,14 @@ class GBExperimentEvaluator {
.toList()
: []);

final hash = const GBUtils().hash(attributeValue + experiment.key);
late final int assigned;
if (hash != null) {
assigned = const GBUtils().chooseVariation(hash, bucketRange);
}
final hash = GBUtils.hash(attributeValue + experiment.key);
final assigned = const GBUtils().chooseVariation(hash, bucketRange);
// If not assigned a variation (assigned === -1), return immediately (not in experiment, variationId 0)
if (assigned == -1) {
return _getExperimentResult(experiment: experiment, gbContext: context);
return _getExperimentResult(
experiment: experiment,
gbContext: context,
);
}

/// If experiment.force is set, return immediately (not in experiment,
Expand All @@ -100,14 +117,16 @@ class GBExperimentEvaluator {
return _getExperimentResult(
experiment: experiment,
variationIndex: forceExp,
inExperiment: false,
gbContext: context,
);
}

// If context.qaMode is true, return immediately (not in experiment, variationId 0)
if (context.qaMode ?? false) {
return _getExperimentResult(experiment: experiment, gbContext: context);
return _getExperimentResult(
experiment: experiment,
gbContext: context,
);
}

final result = _getExperimentResult(
Expand All @@ -116,17 +135,19 @@ class GBExperimentEvaluator {
inExperiment: true,
gbContext: context,
);
context.trackingCallBack!(experiment, result);

context.trackingCallBack?.call(experiment, result);

return result;
}

/// This is a helper method to create an ExperimentResult object.
GBExperimentResult _getExperimentResult(
{required GBContext gbContext,
required GBExperiment experiment,
int variationIndex = 0,
bool inExperiment = false}) {
static GBExperimentResult _getExperimentResult({
required GBContext gbContext,
required GBExperiment experiment,
int variationIndex = 0,
bool inExperiment = false,
}) {
var targetVariationIndex = variationIndex;

// Check whether variationIndex lies within bounds of variations size
Expand All @@ -146,14 +167,14 @@ class GBExperimentEvaluator {
// Hash Attribute - used for Experiment Calculations
final hashAttribute = experiment.hashAttribute ?? Constant.idAttribute;
// Hash Value against hash attribute
final hashValue = gbContext.attributes?[hashAttribute];
final hashValue = gbContext.attributes?[hashAttribute]?.toString() ?? '';

return GBExperimentResult(
inExperiment: inExperiment,
variationID: targetVariationIndex,
value: targetValue,
hasAttributes: hashAttribute,
hashValue: hashValue as String?,
hashValue: hashValue,
);
}
}
35 changes: 20 additions & 15 deletions lib/src/Evaluator/feature_evaluator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import 'package:growthbook_sdk_flutter/growthbook_sdk_flutter.dart';
/// Returns Calculated Feature Result against that key
class GBFeatureEvaluator {
GBFeatureResult evaluateFeature(GBContext context, String featureKey) {
static GBFeatureResult evaluateFeature(GBContext context, String featureKey) {
/// If we are not able to find feature on the basis of the passed featureKey
/// then we are going to return unKnownFeature.
final targetFeature = context.features[featureKey];
Expand Down Expand Up @@ -44,10 +44,10 @@ class GBFeatureEvaluator {
continue;
} else {
// Compute a hash using the Fowler–Noll–Vo algorithm (specifically fnv32-1a)
final hashFNV = GBUtils().hash(attributeValue + featureKey);
final hashFNV = GBUtils.hash(attributeValue + featureKey);
// If the hash is greater than rule.coverage, skip the rule

if (hashFNV != null && hashFNV > rule.coverage!) {
if (hashFNV > rule.coverage!) {
continue;
}
}
Expand All @@ -58,16 +58,20 @@ class GBFeatureEvaluator {
);
} else {
final exp = GBExperiment(
key: rule.key ?? featureKey,
variations: rule.variations ?? [],
coverage: rule.coverage,
weights: rule.weights,
hashAttribute: rule.hashAttribute,
namespace: rule.namespace,
force: rule.force);
key: rule.key ?? featureKey,
variations: rule.variations ?? [],
coverage: rule.coverage,
weights: rule.weights,
hashAttribute: rule.hashAttribute,
namespace: rule.namespace,
force: rule.force,
);

final result = GBExperimentEvaluator.evaluateExperiment(
context: context,
experiment: exp,
);

final result = GBExperimentEvaluator()
.evaluateExperiment(context: context, experiment: exp);
if (result.inExperiment ?? false) {
return _prepareResult(
value: result.value,
Expand All @@ -84,13 +88,14 @@ class GBFeatureEvaluator {
}
// Return (value = defaultValue or null, source = defaultValue)
return _prepareResult(
value: targetFeature.defaultValue,
source: GBFeatureSource.defaultValue);
value: targetFeature.defaultValue,
source: GBFeatureSource.defaultValue,
);
}

/// This is a helper method to create a FeatureResult object.
/// Besides the passed-in arguments, there are two derived values - on and off, which are just the value cast to booleans.
GBFeatureResult _prepareResult(
static GBFeatureResult _prepareResult(
{required dynamic value,
required GBFeatureSource source,
GBExperiment? experiment,
Expand Down
38 changes: 18 additions & 20 deletions lib/src/Features/feature_data_source.dart
Original file line number Diff line number Diff line change
@@ -1,33 +1,31 @@
import 'package:growthbook_sdk_flutter/growthbook_sdk_flutter.dart';

typedef FeatureFetchSuccessCallBack = void Function(
FeaturedDataModel featuredDataModel,
);

abstract class FeaturesFlowDelegate {
void featuresFetchedSuccessfully(GBFeatures gbFeatures);
void featuresFetchFailed(GBError? error);
}

class FeatureDataSource {
FeatureDataSource(
{required this.context, required this.client, required this.onError});
FeatureDataSource({
required this.context,
required this.client,
});
final GBContext context;
final BaseClient client;
final OnError onError;

Future<FeaturedDataModel> fetchFeatures() async {
Future<void> fetchFeatures(
FeatureFetchSuccessCallBack onSuccess, OnError onError) async {
final api = context.hostURL! + Constant.featurePath + context.apiKey!;
await client.consumeGetRequest(api, onSuccess, onError);
setUpModel();
return model;
}

late FeaturedDataModel model;
late Map<String, dynamic> data;

/// Assign response to local variable [data]
void onSuccess(response) {
data = response;
}

/// Initialize [model] from the [data]
void setUpModel() {
model = FeaturedDataModel.fromJson(data);
await client.consumeGetRequest(
api,
(response) => onSuccess(
FeaturedDataModel.fromJson(response),
),
onError,
);
}
}
Loading

0 comments on commit 4232e91

Please sign in to comment.