Look

Look is a Flutter package for Widget preview.

It gives the possibility to start only the widget to preview and not the whole application.

Purpose

Imagine you are working on a Flutter application and you want to work and edit only a specific Widget, page, or whatever.

Most of the concrete applications have many pages, many widgets, and many other things like athentication, state management, routing, API, etc.

Let's suppose that you want to work on a deep page in the app. You have to start the whole app, maybe having to generate some data, beeing connected to an API or a server, etc.

But:

  • You just want to work on a specific widget
  • You don't really need to start the whole app, maybe generate data or be connected to an API or a server with authentication
  • You don't really need to waiste time on all of these things

You main goal is only to work on this deep page.

Look was made for you !

Look package gives you the possibility to run only the widget you want to preview with no pre-requisites.

Getting started

Add look, look_generator and build_runner to your app dependencies:

flutter pub add look
flutter pub add --dev look_generator build_runner

Features

Previewing a widget

Just add the @look annotation to the widget you want to preview.

// my_widget.dart

@look
class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Generate sources using build_runner using the fancy command line that everyone know now:

flutter pub run build_runner build --delete-conflicting-outputs

A brand new file has been generated beside the original one. Search for the file *.look.dart.

Run it, and voilĂ  !

Passing parameters

Use a builder method to pass parameters to your widget.

Use a theme builder method to pass a theme to your widget (Optional).

@Look(
    builder: myBuilderMethod,
    theme: myThemeBuilderMethod, // optional
)
class MyWidget extends StatelessWidget {
  final String text;
  final MyData data;
  final MyController controller;
  final AnyOtherWidget child;

  MyWidget({
    required this.text,
    required this.data,
    required this.controller,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    // ...
  }
}

myBuilderMethod() => MyWidget(
  text: 'Hello World',
  data: MyData(),
  controller: MyController(),
  child: AnyOtherWidget(),
  // ...
);

V1 to V2 Migration:

Just replace @Look('myBuilderMethod')

by @Look(builder: myBuilderMethod)

Golden tests with look

Look is also a golden tests factory.

First, create a new dart file in the test folder.

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:example/pages/home.dart';
import 'package:look/look.dart';

part 'home_page_test.lookgolden.dart';

@LookGolden(type: MyHomePage)
main() => lookGoldens();

Then, generate test sources using the following command:

flutter pub run build_runner build --delete-conflicting-outputs

Look will generate golden tests for you.

// home_page_test.lookgolden.dart
// GENERATED CODE - DO NOT MODIFY BY HAND

// **************************************************************************
// GoldenGenerator
// **************************************************************************

part of 'home_page_test.dart';

lookGoldens() => group('MyHomePage golden tests', () {
      testWidgets('1080x2340 MyHomePage light theme',
          (WidgetTester tester) async {
        tester.binding.window.physicalSizeTestValue =
            const Size(1080.0, 2340.0);
        await tester.pumpWidget(
            MaterialApp(theme: ThemeData(), home: const MyHomePage()));
        await expectLater(find.byType(MyHomePage),
            matchesGoldenFile('goldens/MyHomePage_golden_1080x2340_.png'));
      });
    });

Available options for @LookGolden

Option Type Description
type DartType The type of the widget to test
builder Function Method reference of the builder function
lightTheme String Method reference of the light theme builder function
darkTheme String Method reference of the dark theme builder function
name String The name of the golden file
dimensions List The dimension list of the golden to test (format: 'width:height'; example: '800x600')

Example with options

// test/golden_shape_test.dart
import 'package:example/pages/widgets/shape.dart';
import 'package:example/theme.dart';
import 'package:look/look.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

part 'golden_shape_test.lookgolden.dart';

@LookGolden(
  type: DynamicShape,
  builder: dynamicShapeBuilder,
  lightTheme: lightTheme,
  darkTheme: darkTheme,
  name: 'goldens/DynamicShape_golden.png',
  dimensions: ['400x600', '800x600', '800x1200', '1600x1200'],
)
main() => lookGoldens();

Complex example result

// golden_shape_test.lookgolden.dart
// GENERATED CODE - DO NOT MODIFY BY HAND

// **************************************************************************
// GoldenGenerator
// **************************************************************************

part of 'golden_shape_test.dart';

lookGoldens() => group('DynamicShape golden tests', () {
      testWidgets('400x600 DynamicShape light theme',
          (WidgetTester tester) async {
        tester.binding.window.physicalSizeTestValue = const Size(400.0, 600.0);
        await tester.pumpWidget(
            MaterialApp(theme: lightTheme(), home: dynamicShapeBuilder()));
        await expectLater(find.byType(DynamicShape),
            matchesGoldenFile('goldens/DynamicShape_golden_400x600_.png'));
      });
      testWidgets('800x600 DynamicShape light theme',
          (WidgetTester tester) async {
        tester.binding.window.physicalSizeTestValue = const Size(800.0, 600.0);
        await tester.pumpWidget(
            MaterialApp(theme: lightTheme(), home: dynamicShapeBuilder()));
        await expectLater(find.byType(DynamicShape),
            matchesGoldenFile('goldens/DynamicShape_golden_800x600_.png'));
      });
      testWidgets('800x1200 DynamicShape light theme',
          (WidgetTester tester) async {
        tester.binding.window.physicalSizeTestValue = const Size(800.0, 1200.0);
        await tester.pumpWidget(
            MaterialApp(theme: lightTheme(), home: dynamicShapeBuilder()));
        await expectLater(find.byType(DynamicShape),
            matchesGoldenFile('goldens/DynamicShape_golden_800x1200_.png'));
      });
      testWidgets('1600x1200 DynamicShape light theme',
          (WidgetTester tester) async {
        tester.binding.window.physicalSizeTestValue =
            const Size(1600.0, 1200.0);
        await tester.pumpWidget(
            MaterialApp(theme: lightTheme(), home: dynamicShapeBuilder()));
        await expectLater(find.byType(DynamicShape),
            matchesGoldenFile('goldens/DynamicShape_golden_1600x1200_.png'));
      });
      testWidgets('400x600 DynamicShape dark theme',
          (WidgetTester tester) async {
        tester.binding.window.physicalSizeTestValue = const Size(400.0, 600.0);
        await tester.pumpWidget(
            MaterialApp(theme: darkTheme(), home: dynamicShapeBuilder()));
        await expectLater(find.byType(DynamicShape),
            matchesGoldenFile('goldens/DynamicShape_golden_400x600__dark.png'));
      });
      testWidgets('800x600 DynamicShape dark theme',
          (WidgetTester tester) async {
        tester.binding.window.physicalSizeTestValue = const Size(800.0, 600.0);
        await tester.pumpWidget(
            MaterialApp(theme: darkTheme(), home: dynamicShapeBuilder()));
        await expectLater(find.byType(DynamicShape),
            matchesGoldenFile('goldens/DynamicShape_golden_800x600__dark.png'));
      });
      testWidgets('800x1200 DynamicShape dark theme',
          (WidgetTester tester) async {
        tester.binding.window.physicalSizeTestValue = const Size(800.0, 1200.0);
        await tester.pumpWidget(
            MaterialApp(theme: darkTheme(), home: dynamicShapeBuilder()));
        await expectLater(
            find.byType(DynamicShape),
            matchesGoldenFile(
                'goldens/DynamicShape_golden_800x1200__dark.png'));
      });
      testWidgets('1600x1200 DynamicShape dark theme',
          (WidgetTester tester) async {
        tester.binding.window.physicalSizeTestValue =
            const Size(1600.0, 1200.0);
        await tester.pumpWidget(
            MaterialApp(theme: darkTheme(), home: dynamicShapeBuilder()));
        await expectLater(
            find.byType(DynamicShape),
            matchesGoldenFile(
                'goldens/DynamicShape_golden_1600x1200__dark.png'));
      });
    });

Libraries

look