ScreenStack SDK
Automated screenshot capture for Flutter apps. Capture app screens during CI/CD builds and upload them to ScreenStack device frames for beautiful app store screenshots.
Features
- Automated capture: Navigate to routes and capture screenshots automatically
- Multi-canvas support: Upload to iOS, Android, and custom canvases simultaneously
- Type-safe config: Dart-first configuration with full IDE support
- Manifest generation: JSON manifest for FlightStack agent integration
- Validation: Catch config errors before running expensive CI builds
Installation
Add to your pubspec.yaml:
dev_dependencies:
screenstack_sdk: ^1.0.0
Quick Start (Simple Mode)
1. Create Configuration
Create lib/screenstack_config.dart:
import 'package:screenstack_sdk/screenstack_sdk.dart';
import 'main.dart';
// Simple mode - route-to-frame mapping configured in FlightStack UI
final screenStackConfig = ScreenStackConfig(
appBuilder: () => const MyApp(),
routes: {
'/': 'home_screen',
'/login': 'login_screen',
'/settings': 'settings_screen',
},
);
2. Create Integration Test
Create integration_test/screenshot_test.dart:
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:screenstack_sdk/screenstack_sdk.dart';
import 'package:my_app/screenstack_config.dart';
void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Capture ScreenStack screenshots', (tester) async {
final result = await captureScreenStackScreenshots(
tester,
binding,
screenStackConfig,
);
expect(result.allSuccessful, isTrue);
});
}
3. Configure FlightStack Pipeline
Add a ScreenStack job to your FlightStack pipeline with upload mode enabled.
Advanced Mode (Multi-Canvas)
For projects targeting multiple app stores (iOS + Android) or multiple locales, use advanced mode to define exactly where each screenshot goes:
import 'package:screenstack_sdk/screenstack_sdk.dart';
import 'main.dart';
final screenStackConfig = ScreenStackConfig.advanced(
appBuilder: () => const MyApp(),
screens: [
// Home screen → uploads to both iOS and Android canvases
Screen(
route: '/home',
name: 'home',
targets: [
FrameTarget(canvas: 'iOS App Store', view: 'Home'),
FrameTarget(canvas: 'Google Play', view: 'Home'),
],
),
// Login screen → uploads to both canvases
Screen(
route: '/login',
name: 'login',
targets: [
FrameTarget(canvas: 'iOS App Store', view: 'Login'),
FrameTarget(canvas: 'Google Play', view: 'Login'),
],
),
// Settings with custom settle delay for animations
Screen(
route: '/settings',
name: 'settings',
settleDelay: Duration(seconds: 1),
targets: [
FrameTarget(canvas: 'iOS App Store', view: 'Settings'),
FrameTarget(canvas: 'Google Play', view: 'Settings'),
],
),
],
);
String Path Syntax
For more concise definitions, use Screen.withPaths:
Screen.withPaths(
route: '/home',
targetPaths: [
'iOS App Store/Home', // canvas/view format
'Google Play/Home',
],
)
ScreenStack Hierarchy
Understanding the ScreenStack structure helps with configuration:
Project (e.g., "My App")
├── Canvas: "iOS App Store"
│ ├── View: "Home"
│ │ ├── Device Frame: iPhone 15 Pro
│ │ ├── Device Frame: iPhone 15 Plus
│ │ └── Device Frame: iPad Pro
│ ├── View: "Login"
│ └── View: "Settings"
│
└── Canvas: "Google Play"
├── View: "Home"
│ ├── Device Frame: Pixel 8
│ └── Device Frame: Pixel Tablet
├── View: "Login"
└── View: "Settings"
Key insight: When you target FrameTarget(canvas: 'iOS App Store', view: 'Home'), the screenshot uploads to all device frames within that view. This means one screenshot automatically fills iPhone 15 Pro, iPhone 15 Plus, and iPad Pro frames.
Workflow
┌─────────────────────────────────────────────────────────────────┐
│ FlightStack CI/CD │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. CAPTURE 2. UPLOAD │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Run Flutter │ │ Upload to │ │
│ │ integration │────────────▶│ ScreenStack │ │
│ │ test │ │ device frames│ │
│ └──────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Screenshots │ │ ScreenStack │ │
│ │ + manifest │ │ composites │ │
│ └──────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ 3. EXPORT/PUBLISH │
│ ┌──────────────┐ │
│ │ App Store │ │
│ │ Google Play │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
API Reference
ScreenStackConfig
Main configuration class.
| Property | Type | Description |
|---|---|---|
appBuilder |
Widget Function() |
Returns your app's root widget |
routes |
Map<String, String>? |
Simple mode: route → screenshot name |
screens |
List<Screen>? |
Advanced mode: screens with targets |
settleDelay |
Duration |
Default delay after navigation (500ms) |
projectId |
String? |
Optional ScreenStack project ID |
Constructors:
ScreenStackConfig()- Simple mode with route mappingScreenStackConfig.advanced()- Advanced mode with explicit targets
Screen
Defines a screen to capture and where to upload it.
| Property | Type | Description |
|---|---|---|
route |
String |
Route path to navigate to |
name |
String? |
Screenshot filename (auto-generated if null) |
targets |
List<FrameTarget> |
Upload destinations |
settleDelay |
Duration? |
Override global settle delay |
Constructors:
Screen()- Standard constructorScreen.withPaths()- Convenience constructor using string paths
FrameTarget
Specifies where to upload a screenshot.
| Property | Type | Description |
|---|---|---|
canvas |
String |
Canvas name in ScreenStack |
view |
String |
View name within canvas |
Constructors:
FrameTarget()- Standard constructorFrameTarget.parse()- Parse from "canvas/view" string
CaptureSessionResult
Returned from captureScreenStackScreenshots().
| Property | Type | Description |
|---|---|---|
results |
List<CaptureResult> |
Results per screen |
allSuccessful |
bool |
True if all captures succeeded |
successCount |
int |
Number of successful captures |
failureCount |
int |
Number of failed captures |
Methods:
writeManifest()- Write JSON manifest for agent
Manifest Format
The SDK generates manifest.json for the FlightStack agent:
{
"outputDir": "build/screenstack_screenshots",
"capturedAt": "2024-01-15T10:30:00.000Z",
"totalScreens": 3,
"successful": 3,
"failed": 0,
"screens": [
{
"route": "/home",
"name": "home",
"filePath": "build/screenstack_screenshots/home.png",
"success": true,
"targets": ["iOS App Store/Home", "Google Play/Home"]
}
]
}
Validation
Validate your config before running CI:
final errors = screenStackConfig.validate();
if (errors.isNotEmpty) {
print('Configuration errors:');
for (final error in errors) {
print(' - $error');
}
}
Handling Authentication
Most apps require login to access certain screens. Here's how to handle it:
Option 1: Setup/Teardown Callbacks (Recommended)
Use the setup callback to authenticate before screenshots:
testWidgets('Capture screenshots', (tester) async {
await captureScreenStackScreenshots(
tester,
binding,
screenStackConfig,
setup: (tester, context) async {
// YOUR AUTH LOGIC - examples:
// Firebase Auth
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: Platform.environment['TEST_EMAIL']!,
password: Platform.environment['TEST_PASSWORD']!,
);
// Supabase
await Supabase.instance.client.auth.signInWithPassword(
email: 'test@example.com',
password: 'testpass',
);
// Custom auth service
await AuthService.signIn(testCredentials);
await tester.pumpAndSettle();
},
teardown: (tester, context) async {
await AuthService.signOut();
},
);
});
Option 2: Demo/Screenshot Mode
Detect screenshot capture and bypass auth entirely:
// In your app's initialization
void main() {
final isScreenshotMode = Platform.environment['SCREENSTACK_CAPTURE'] == 'true';
runApp(MyApp(
// Use mock data for screenshots
authOverride: isScreenshotMode ? MockAuthProvider() : null,
));
}
This gives you perfect control over screenshot content.
Option 3: Test-Specific Routes
Create unauthenticated versions of screens for testing:
// In your router
GoRoute(
path: '/preview/dashboard', // No auth required
builder: (context, state) => DashboardPage(
user: DemoUser(),
data: DemoData(),
),
),
Then capture /preview/dashboard instead of /dashboard.
Environment Variables
For secrets, use your CI/CD pipeline's secrets feature:
- GitHub Actions: Repository secrets
- FlightStack: Pipeline environment variables
- GitLab CI: CI/CD variables
The agent passes all environment variables to your integration test.
Best Practices
1. Match naming conventions
Use consistent naming between routes and views:
- Route
/home→ ViewHome - Route
/settings→ ViewSettings
2. Handle async data loading
Set appropriate settleDelay for screens that fetch data:
Screen(
route: '/dashboard',
settleDelay: Duration(seconds: 2), // Wait for API calls
targets: [...],
)
3. Test locally first
Run the integration test locally before pushing:
flutter test integration_test/screenshot_test.dart
4. Keep routes simple
Avoid routes requiring specific IDs or authentication state. For authenticated screens, set up mock auth in your test:
testWidgets('Capture screenshots', (tester) async {
// Set up mock authentication
await MockAuthService.signInAsTestUser();
final result = await captureScreenStackScreenshots(...);
});
5. Use const for better performance
const screenStackConfig = ScreenStackConfig(
appBuilder: MyApp.new,
routes: {
'/': 'home',
},
);
Troubleshooting
"Route not found" errors
Ensure routes are registered in your router:
- go_router: Check
GoRoutepath definitions - Navigator: Check
onGenerateRouteor named routes map
Screenshots are blank or incomplete
Increase settleDelay to allow widgets to fully build:
Screen(
route: '/home',
settleDelay: Duration(seconds: 2),
targets: [...],
)
Navigation fails entirely
The SDK tries go_router first, then falls back to Navigator. Ensure your app uses standard navigation:
// go_router - SDK calls this:
GoRouter.of(context).go('/home');
// Navigator - SDK falls back to:
Navigator.of(context).pushNamed('/home');
Config validation errors
Run validation to catch issues early:
void main() {
final errors = screenStackConfig.validate();
assert(errors.isEmpty, 'Config errors: $errors');
}
Requirements
- Flutter 3.10.0 or higher
- Dart 3.5.0 or higher
- FlightStack CI/CD (for automated uploads)
Related
- ScreenStack - App store screenshot designer
- FlightStack - CI/CD for Flutter apps
- VooStack - Flutter developer tools
Built by VooStack
Need help with Flutter development or custom data grid solutions?
VooStack builds enterprise Flutter applications and developer tools. We're here to help with your next project.
License
MIT License - see LICENSE for details.
Libraries
- screenstack_sdk
- ScreenStack SDK - Automated screenshot capture for Flutter apps.