screenstack_sdk 2.0.2
screenstack_sdk: ^2.0.2 copied to clipboard
Automated screenshot capture for Flutter apps. Configure routes, capture screens during CI/CD builds, and upload to ScreenStack device frames for app store assets.
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.