This repository introduces abstractions around native review capabilities to ease code sharing and testability. It also introduces business logic to quickly configure conditions and state tracking to prompt for reviews at the right moment.
To be replaced by the actual pub.dev tags once merged.
Before getting started, please read the Android and iOS application review documentation.
-
Add the
review_service
package to your project. -
Create an instance of
ReviewService
. We'll cover dependency injection in details later on in this documentation.var reviewConditionsBuilder = ReviewConditionsBuilderImplementation .empty() .minimumPrimaryActionsCompleted(1); var reviewPrompter = LoggingReviewPrompter(); var reviewSettingsSource = MemoryReviewSettingsSource<ReviewSettings>(() => const ReviewSettings()); var reviewService = ReviewService<ReviewSettings>( logger: Logger(), reviewPrompter: reviewPrompter, reviewSettingsSource: reviewSettingsSource, reviewConditionsBuilder: reviewConditionsBuilder);
-
Use the service.
- Update the review settings based on application events.
late ReviewService<ReviewSettings> _reviewService; Future<void> doPrimaryAction() async { // Do Primary Action. // Track this action. await _reviewService.trackPrimaryActionCompleted(); }
- Use the service to request review.
late ReviewService<ReviewSettings> _reviewService; Future<void> onCompletedImportantFlow() async { // Do Meaningful Task. // Check if all conditions are satisfied and prompt for review if they are. await _reviewService.tryRequestReview(); }
- Update the review settings based on application events.
MemoryReviewSettingsSource
is great for automated testing but should not be the implementation of choice for real use-cases. Instead, you should create your own implementation that persists data on the device (so that review settings don't reset when you kill the app).
/// Storage implementation of <see cref="ReviewSettingsSource{TReviewSettings}"/>.
sealed class StorageReviewSettingsSource<ReviewSettings> extends ReviewSettings
implements ReviewSettingsSource<ReviewSettings> {
@override
Future<ReviewSettings> read() {
// TODO: Return stored review settings.
return Future.value(ReviewSettings());
}
@override
Future<void> write(ReviewSettings reviewSettings) {
// TODO: Update stored review settings.
return Future.value();
}
}
Here is a simple code that does dependency injection using get_it
.
GetIt getIt = GetIt.instance;
// Register the ReviewPrompter implementation with GetIt
getIt.registerSingleton<ReviewPrompter>(ReviewPrompter());
Now that everything is setup, Let's see what else we can do!
To track the provided review settings you can use the following ReviewService
extensions.
💡 The review request count and the last review request are automatically tracked by the service.
- TrackApplicationLaunched : Tracks that the application was launched (Also tracks if it's the first launch).
- TrackPrimaryActionCompleted : Tracks that a primary action was completed.
- TrackSecondaryActionCompleted : Tracks that a secondary action was completed.
- PrimaryActionCompletedCount : The number of primary actions completed.
- SecondaryActionCompletedCount : The number of secondary actions completed.
- ApplicationLaunchCount : The number of times the application has been launched.
- FirstApplicationLaunch : When the application first started.
- RequestCount : The number of review requested.
- LastRequest : When the last review was requested.
If you want to use our default review conditions, you can use ReviewConditionsBuilder.defaultBuilder()
and pass it to the ReviewService
constructor, or register it as a transient dependency when using dependency injection. Please note that our review conditions are also generic, so they can be used with custom review settings too.
The ReviewConditionsBuilder.Default()
extension method uses the following conditions.
- 3 application launches required.
- 2 completed primary actions.
- 5 days since the first application launch.
- 15 days since the last review request.
- MinimumPrimaryActionsCompleted : Make sure that it prompts for review only if the number of completed primary actions meets the minimum.
- MinimumSecondaryActionsCompleted : Make sure that it prompts for review only if the number of completed secondary actions meets the minimum.
- MinimumApplicationLaunchCount : Make sure that it prompts for review only if the number of times the application has been launched meets the required minimum.
- MinimumElapsedTimeSinceApplicationFirstLaunch : Make sure that it prompts for review only if the elapsed time since the first application launch meets the required minimum.
- MinimumElapsedTimeSinceLastReviewRequest : Make sure that it prompts for review only if the elapsed time since the last review request meets the required minimum.
- Custom : Custom condition made with a synchronous lambda function.
- CustomAsync : Custom asynchronous condition made with an asynchronous lambda function.
To create custom review conditions, you have to use ReviewConditionsBuilder.custom
and ReviewConditionsBuilder.customAsync
and provide them with a function directly instead of a condition. Also you can create extensions for ReviewConditionsBuilder
and add a new condition to the builder. To create a review condition, you can use both SynchronousReviewCondition
and AsynchronousReviewCondition
you need to provide them with a function.
/// Extensions for ReviewConditionsBuilder<TReviewSettings>.
extension ReviewConditionsBuilderExtensions
on ReviewConditionsBuilder<ReviewSettingsCustom> {
/// The application onboarding must be completed.
ReviewConditionsBuilder applicationOnboardingCompleted(
ReviewConditionsBuilder builder) {
builder.conditions.add(SynchronousReviewCondition<ReviewSettingsCustom>((reviewSettings, currentDateTime) => reviewSettings.hasCompletedOnboarding == true));
return builder;
}
}
Here is a simple code that uses the builder extensions for review conditions.
var reviewConditionsBuilder = ReviewConditionsBuilder.empty()
.minimumPrimaryActionsCompleted(1)
.minimumSecondaryActionsCompleted(1)
.minimumApplicationLaunchCount(1)
.minimumElapsedTimeSinceApplicationFirstLaunch(Duration(days: 1))
.custom((reviewSettings, currentDateTime) {
return reviewSettings.primaryActionCompletedCount +
reviewSettings.secondaryActionCompletedCount >=
2;
});
It's possible to customize the review conditions used by the service by using ReviewConditionsBuilder
and passing it to the ReviewService
constructor or by injecting it as a transient when using dependency injection.
var reviewConditionsBuilder =
ReviewConditionsBuilder.empty<ReviewSettingsCustom>()
.minimumPrimaryActionsCompleted(3)
.minimumApplicationLaunchCount(3)
.minimumElapsedTimeSinceApplicationFirstLaunch(Duration(days: 5))
.custom((reviewSettings, currentDateTime) {
return reviewSettings.primaryActionCompletedCount +
reviewSettings.secondaryActionCompletedCount >=
2;
});
First let's declare a new ReviewSettings
named CustomReviewSettings
with a favoriteJokesCount
to track how many jokes were favorited in an hypothetical dad jokes application
class CustomReviewSettings extends ReviewSettings {
final int favoriteJokesCount;
const CustomReviewSettings({
this.favoriteJokesCount = 0,
super.primaryActionCompletedCount = 0,
super.secondaryActionCompletedCount = 0,
super.applicationLaunchCount = 0,
super.firstApplicationLaunch,
super.requestCount = 0,
super.lastRequest,
});
@override
CustomReviewSettings copyWith({
int? primaryActionCompletedCount,
int? secondaryActionCompletedCount,
int? applicationLaunchCount,
DateTime? firstApplicationLaunch,
int? requestCount,
DateTime? lastRequest,
}) {
return CustomReviewSettings(
primaryActionCompletedCount:
primaryActionCompletedCount ?? this.primaryActionCompletedCount,
secondaryActionCompletedCount:
secondaryActionCompletedCount ?? this.secondaryActionCompletedCount,
applicationLaunchCount:
applicationLaunchCount ?? this.applicationLaunchCount,
firstApplicationLaunch:
firstApplicationLaunch ?? this.firstApplicationLaunch,
requestCount: requestCount ?? this.requestCount,
lastRequest: lastRequest ?? this.lastRequest,
favoriteJokesCount: favoriteJokesCount,
);
}
CustomReviewSettings copyWithFavorite(int favoriteCount) {
return CustomReviewSettings(
primaryActionCompletedCount: super.primaryActionCompletedCount,
secondaryActionCompletedCount: super.secondaryActionCompletedCount,
applicationLaunchCount: super.applicationLaunchCount,
firstApplicationLaunch: super.firstApplicationLaunch,
requestCount: super.requestCount,
lastRequest: super.lastRequest,
favoriteJokesCount: favoriteCount,
);
}
}
âš Notes :
- It's important you define a
copyWith
that overrides the one from theReviewSettings
class while returning the newly added property. This ensures that the calls made to thecopyWith
method ofReviewSettings
will be overriden by your implementation- You need to define a
copyWith
method that returns the superclassReviewSettings
value along with your new property passed as a parameter. This method will be used when you will track your review settings further down the line.
Once you've defined your new custom review settings, it is recommended to add an extension to the ReviewConditionsBuilder
for uniformity :
extension CustomReviewConditionsBuilderExtensions
on ReviewConditionsBuilder<CustomReviewSettings> {
ReviewConditionsBuilder<CustomReviewSettings> minimumJokesFavorited(
int minimumJokesFavorited,
) {
conditions.add(
SynchronousReviewCondition<CustomReviewSettings>(
(reviewSettings, currentDateTime) =>
reviewSettings.favoriteJokesCount >= minimumJokesFavorited,
),
);
return this;
}
}
âš Notes : You need to specify that the
ReviewConditionsBuilder
takes your newCustomReviewSettings
and not the defaultReviewSettings
since you want to use your newly added property.
Then you need to register your service with the extended ReviewConditionsBuilder
and the new review settings like this :
var logger = Logger();
var reviewConditionsBuilder =
ReviewConditionsBuilderImplementation<CustomReviewSettings>()
.minimumJokesFavorited(3);
var reviewSettingsSource =
GetIt.I.registerSingleton<ReviewSettingsSource<CustomReviewSettings>>(
MemoryReviewSettingsSource<CustomReviewSettings>(
() => const CustomReviewSettings(),
),
);
GetIt.I.registerSingleton(
ReviewService<CustomReviewSettings>(
logger: logger,
reviewPrompter: ReviewPrompter(logger: logger),
reviewSettingsSource: reviewSettingsSource,
reviewConditionsBuilder: reviewConditionsBuilder,
),
);
âš Notes :
- You need to specify again that you want the conditions builder the settings source and the service to use your
CustomReviewSettings
and not the generic.- We recommend that you define your own interface that wraps
ReviewService<CustomReviewSettings>
to make the usage code leaner and ease any potential refactorings.
/// This interface wraps ReviewService<CustomReviewSettings> so that you don't have to repeat the generic parameter everywhere that you would use the review service.
/// In other words, you should use this interface in the app instead of ReviewService<CustomReviewSettings> because it's leaner.
class CustomReviewService implements ReviewService<CustomReviewSettings> {
late ReviewService<CustomReviewSettings> _reviewService;
CustomReviewService(ReviewService<CustomReviewSettings> reviewService) {
_reviewService = reviewService;
}
@override
Future<bool> getAreConditionsSatisfied() async {
return await _reviewService.getAreConditionsSatisfied();
}
@override
Future<void> tryRequestReview() async {
await _reviewService.tryRequestReview();
}
@override
Future<void> updateReviewSettings(
CustomReviewSettings Function(CustomReviewSettings p1) updateFunction,
) async {
await _reviewService.updateReviewSettings(updateFunction);
}
}
After creating your own implementation of the ReviewService you just need to register it like this :
GetIt.I.registerSingleton(
CustomReviewService(
ReviewService<CustomReviewSettings>(
logger: logger,
reviewPrompter: ReviewPrompter(logger: logger),
reviewSettingsSource: reviewSettingsSource,
reviewConditionsBuilder: reviewConditionsBuilder,
),
),
);
And use it like that :
await GetIt.I.get<CustomReviewService>().trackFavoriteJokesCount();
Now you're all set to track how many jokes are favorited in your app and prompt for a review once your review conditions are meant (3 favorite jokes in this case).
This is what you need to know before testing and debugging this service. Please note that this may change and you should always refer to the Apple and Android documentation for the most up-to-date information.
- You can't test this service while debugging the application, the prompt won't show up. To test it, you need to use the internal application sharing or the internal testing feature in Google Play Console. See this for more details.
- You can't use a Google Suite account on Google Play to review an application because the prompt will not show up.
- You can test on a real device or on a simulator.
- You can test this service only while debugging the application (It won't show up on TestFlight).
Take a look at in_app_review that we use to prompt for review.
Please consult BREAKING_CHANGES.md for more information about version history and compatibility.
This project is licensed under the Apache 2.0 license - see the LICENSE file for details.
Please read CONTRIBUTING.md for details on the process for contributing to this project.
Be mindful of our Code of Conduct.