ashlarui

Empty

What to show when there is nothing here yet.

Empty, as it looks in an app Empty, as it looks in an app, dark
import 'package:ashlar_icons/ashlar_icons.dart';
import 'package:flutter/widgets.dart';

import 'ui/button.dart';
import 'ui/empty.dart';

class EmptyDemo extends StatelessWidget {
  const EmptyDemo({super.key});

  @override
  Widget build(BuildContext context) => Empty(
    icon: const AshlarIcon(AshlarIconsRoundedStroke.folder),
    title: const Text('No projects yet'),
    description: const Text('You have not made a project. Make your first one to get started.'),
    actions: [
      Button(onPressed: () {}, child: const Text('Create project')),
      Button(variant: ButtonVariant.outline, onPressed: () {}, child: const Text('Import')),
    ],
  );
}

Try its options

Each option its constructor takes, to change and see; the code below it is what you chose, to copy.

Empty with controls, as it looks in an app Empty with controls, as it looks in an app, dark

Add it

In your Flutter project:

$ ashlar add empty

It writes theme.dart, palette.dart and empty.dart into lib/ui, keeping any you already have.

Examples

Other ways to use it, each running live, with its code.

On a muted fill

With a border and a background.

On a muted fill, as it looks in an app On a muted fill, as it looks in an app, dark
// On a muted fill: With a border and a background.
import 'package:ashlar_icons/ashlar_icons.dart';
import 'package:flutter/widgets.dart';

import 'ui/button.dart';
import 'ui/empty.dart';

class EmptyMutedExample extends StatelessWidget {
  const EmptyMutedExample({super.key});

  @override
  Widget build(BuildContext context) => SizedBox(
    width: 420,
    child: Empty(
      background: true,
      border: true,
      icon: const AshlarIcon(AshlarIconsRoundedStroke.folderOpen),
      title: const Text('No projects yet'),
      description: const Text('Make your first one to get started.'),
      actions: [Button(onPressed: () {}, child: const Text('New project'))],
    ),
  );
}

API

Read from the source above: each parameter with its type, its default, and what it is for.

Emptyclass

What a list or a page shows when there is nothing in it yet, and what to do about it.

Parameter Type Default What it is for
icon Widget? Drawn in a small muted square above the title.
titlerequired Widget What is empty.
descriptionWidget?What to do about it.
actions List<Widget> const [] Buttons to do it.
background bool false On a muted ground, filling the width it is given.
border bool false Inside a line, filling the width it is given.

Source

What lands in your app. It is yours from then on: change it as you would your own code, and ashlar add will not write over it.

lib/ui/empty.dart 116 lines
// Ashlar UI: Empty. Copied into your app by `ashlar add empty`; it is yours to change.

import 'package:flutter/widgets.dart';

import 'theme.dart';

/// What a list or a page shows when there is nothing in it yet, and what to do about it.
///
/// ```dart
/// Empty(
///   icon: AshlarIcon(AshlarIconsRoundedStroke.folder),
///   title: Text('No projects yet'),
///   description: Text('Create a project to get started.'),
///   actions: [Button(onPressed: create, child: Text('Create project'))],
/// )
/// ```
class Empty extends StatelessWidget {
  const Empty({
    super.key,
    this.icon,
    required this.title,
    this.description,
    this.actions = const [],
    this.background = false,
    this.border = false,
  });

  /// Drawn in a small muted square above the title.
  final Widget? icon;

  /// What is empty.
  final Widget title;

  /// What to do about it.
  final Widget? description;

  /// Buttons to do it.
  final List<Widget> actions;

  /// On a muted ground, filling the width it is given.
  final bool background;

  /// Inside a line, filling the width it is given.
  final bool border;

  @override
  Widget build(BuildContext context) {
    final t = AshlarTheme.of(context);
    final c = t.colors;
    final framed = background || border;
    final content = ConstrainedBox(
      constraints: const BoxConstraints(maxWidth: 384),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          if (icon != null) ...[
            Container(
              width: 40,
              height: 40,
              alignment: Alignment.center,
              decoration: BoxDecoration(
                color: c.muted,
                borderRadius: BorderRadius.circular(t.radiusLg),
              ),
              child: IconTheme.merge(
                data: IconThemeData(color: c.foreground, size: 24),
                child: icon!,
              ),
            ),
            const SizedBox(height: 16),
          ],
          DefaultTextStyle.merge(
            style: t.text.lg.copyWith(
              color: c.foreground,
              fontWeight: FontWeight.w500,
            ),
            textAlign: TextAlign.center,
            child: Semantics(header: true, child: title),
          ),
          if (description != null) ...[
            const SizedBox(height: 8),
            DefaultTextStyle.merge(
              style: t.text.sm.copyWith(
                color: c.mutedForeground,
                height: 22 / 14,
              ),
              textAlign: TextAlign.center,
              child: description!,
            ),
          ],
          if (actions.isNotEmpty) ...[
            const SizedBox(height: 24),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              alignment: WrapAlignment.center,
              children: actions,
            ),
          ],
        ],
      ),
    );
    if (!framed) return content;
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
      decoration: BoxDecoration(
        color: background ? c.muted.withValues(alpha: c.muted.a * .5) : null,
        borderRadius: BorderRadius.circular(t.radiusLg),
        border: border ? Border.all(color: c.border) : null,
      ),
      // Centred across, and as tall as what it holds.
      child: Center(heightFactor: 1, child: content),
    );
  }
}