Every app developer has to submit some kind of screenshot for potential users. This task can be tedious, difficult, time-consuming. Maybe you don't want to setup your app in certain ways so that it works, or looks a specific way.
Flutter is in all regards flexible, it offers great tooling for everything.
So what if, instead of writing tests, we want to create screenshots?
First of all, taking screenshots automatically is not something Flutter has straight out of the box, so we are required to duct-tape some things together to make it work.
In my case, I have created a class called Bootstrapper
which handles all the work of providing a "similar to production" environment.
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:shared_preferences/shared_preferences.dart';
class Bootstrapper {
final AppPreferences preferences;
final String? locale;
const Bootstrapper._(this.preferences, this.locale);
static Future<Bootstrapper> getInstance(String? locale) async {
// do asynchronous initialization things here
// ignore: invalid_use_of_visible_for_testing_member
SharedPreferences.setMockInitialValues({});
final sharedPreferences = await SharedPreferences.getInstance();
final preferences = AppPreferences(sharedPreferences);
return Bootstrapper._(preferences, locale);
}
Widget wrap(Widget child) {
return ProviderScope(
overrides: [
preferencesProvider.overrideWith((ref) => preferences),
],
child: MaterialApp(
home: child,
debugShowCheckedModeBanner: false,
useInheritedMediaQuery: true, // unsure if needed
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: locale != null ? Locale(locale!) : const Locale("en"),
theme: yourDefaultAppTheme,
),
);
}
}
With the Bootstrapper
we cut down on writing the same code over and over.
Since Flutter does not provide an easy to use functions to capture screenshots, we need to write our own.
Future<Uint8List> takeScreenshot<T extends Widget>() async {
final element = find.byType(T, skipOffstage: false).evaluate().first;
final image = await captureImage(element);
final data = await image.toByteData(format: ui.ImageByteFormat.png);
return data!.buffer.asUint8List();
}
Future<ui.Image> captureImage(Element element) {
assert(element.renderObject != null);
var renderObject = element.renderObject!;
while (!renderObject.isRepaintBoundary) {
renderObject = renderObject.parent! as RenderObject;
}
assert(!renderObject.debugNeedsPaint);
final layer = renderObject.debugLayer! as OffsetLayer;
return layer.toImage(renderObject.paintBounds);
}
extension SetScreenSize on WidgetTester {
Future<void> setScreenSize(Size size, [double pixelDensity = 1]) async {
await binding.setSurfaceSize(size);
binding.window.physicalSizeTestValue = size;
binding.window.devicePixelRatioTestValue = pixelDensity;
}
}
Future<void> main() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
late Uint8List screenshot;
testWidgets(
'Main screen',
(tester) async {
// Setup
await tester.setScreenSize(screenSize, screenDensity);
final bootstrapper = await Bootstrapper.getInstance(locale);
// Start our scene
runApp(
bootstrapper.wrap(const MainScreen()),
);
await tester.pumpAndSettle();
// Take a screenshot
screenshot = await takeScreenshot<MainScreen>();
},
);
tearDownAll(() async {
await File("screenshot.png").writeAsBytes(screenshot, flush: true);
});
}