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.

pub package License: MIT

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 mapping
  • ScreenStackConfig.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 constructor
  • Screen.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 constructor
  • FrameTarget.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:

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 → View Home
  • Route /settings → View Settings

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 GoRoute path definitions
  • Navigator: Check onGenerateRoute or named routes map

Screenshots are blank or incomplete

Increase settleDelay to allow widgets to fully build:

Screen(
  route: '/home',
  settleDelay: Duration(seconds: 2),
  targets: [...],
)

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)

Built by VooStack

Need help with Flutter development or custom data grid solutions?

Contact Us

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.