flutter_empty_state 0.1.1 copy "flutter_empty_state: ^0.1.1" to clipboard
flutter_empty_state: ^0.1.1 copied to clipboard

Lightweight, theme-aware empty, error, no-internet, search-empty and loading state widgets for Flutter. Customizable and dependency-free.

flutter_empty_state #

pub package CI license: MIT

Stop rewriting the same Center(child: Text('No data found')) in every screen.

flutter_empty_state is a tiny, dependency-free set of widgets for the states every app has but nobody enjoys building: empty, error, offline, empty search, and loading. They use your app's theme out of the box, look right in light and dark, and stay fully customisable when you need them to.

return items.isEmpty
    ? const EmptyState(title: 'No products yet')
    : ProductList(items: items);

Preview #

Empty Error No internet
Search empty Skeleton loader Dark mode

Features #

  • ðŸŠķ Zero dependencies — pure Flutter, nothing extra to audit or update.
  • ðŸŽĻ Theme-aware — colours and text styles come from your ThemeData, so light/dark just works.
  • ðŸ§Đ Five ready-made states — EmptyState, ErrorState, NoInternetState, SearchEmptyState, LoadingState.
  • 🔀 StateView — render the right widget from a single ViewState value, with a cross-fade between states.
  • 🌍 Global styling — set your defaults once with an EmptyStateTheme, override per widget when needed.
  • âœĻ Subtle entrance animation — a tasteful fade + slide that respects the OS "reduce motion" setting.
  • 💀 Skeleton loader — a built-in shimmer placeholder list for a more polished loading state.
  • â™ŋ Accessible — real text widgets, decorative icons, and a live region on the loading state.
  • ðŸ“Ķ Drops in anywhere — Scaffold, Center, Column and ListView all work without layout gymnastics.
  • ✅ Null-safe and covered by widget tests.

Installation #

Add it to your pubspec.yaml:

dependencies:
  flutter_empty_state: ^0.1.0

Then run flutter pub get and import it:

import 'package:flutter_empty_state/flutter_empty_state.dart';

Basic usage #

Every widget works on its own with zero required parameters, so you can start small and customise later:

const EmptyState();        // sensible defaults
const LoadingState();      // a centered spinner

The widgets #

EmptyState #

EmptyState(
  icon: Icons.inbox_outlined,
  title: 'No data found',
  message: 'There is nothing to show here yet.',
  actionText: 'Refresh',
  onAction: () => _reload(),
)

The action button shows up only when you pass both actionText and onAction.

ErrorState #

ErrorState(
  title: 'Something went wrong',
  message: 'Please try again later.',
  actionText: 'Retry',
  onAction: () => _retry(),
)

title, message and actionText already default to error-friendly copy, so ErrorState(onAction: _retry) is enough for a working retry screen.

NoInternetState #

NoInternetState(
  onRetry: () => _reconnect(),
)

SearchEmptyState #

SearchEmptyState(
  query: 'iPhone',
  onClear: () => _clearSearch(),
)

When you don't pass a message, it builds one from the query — e.g. No matches for "iPhone".

LoadingState #

LoadingState(
  message: 'Loading...',
)

Skeleton loading #

For a more polished wait, swap the spinner for a shimmering placeholder list:

StateView(
  state: state,
  loading: const SkeletonList(),   // shimmer instead of a spinner
  child: ProductList(items: items),
)

Need a custom shape? Compose your own from the Skeleton and Shimmer primitives:

Shimmer(
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: const [
      Skeleton.circle(size: 56),
      SizedBox(height: 12),
      Skeleton(height: 18),           // fills the width
      SizedBox(height: 8),
      Skeleton(width: 180, height: 14),
    ],
  ),
)

The shimmer is theme-aware (looks right in light and dark) and freezes to a static placeholder when "reduce motion" is on.

StateView #

Describe each state once and switch on a single value instead of juggling if/else chains in your build method:

StateView(
  state: viewState, // a ViewState
  loading: const LoadingState(),
  empty: const EmptyState(title: 'No products found'),
  error: ErrorState(onAction: _retry),
  noInternet: NoInternetState(onRetry: _retry),
  child: ProductList(items: items),
)

ViewState is just an enum:

enum ViewState { loading, empty, error, noInternet, content }

Any slot you leave out falls back to a default widget, so you only fill in what you want to change. A typical controller flow looks like this:

ViewState state = ViewState.loading;

Future<void> load() async {
  setState(() => state = ViewState.loading);
  try {
    items = await api.fetchProducts();
    setState(() => state = items.isEmpty ? ViewState.empty : ViewState.content);
  } on SocketException {
    setState(() => state = ViewState.noInternet);
  } catch (_) {
    setState(() => state = ViewState.error);
  }
}

Custom styling #

Nothing is locked down. Override as little or as much as you like:

EmptyState(
  icon: Icons.favorite_border,
  iconSize: 96,
  iconColor: Colors.pink,
  title: 'No favourites yet',
  message: 'Tap the heart on any item to save it here.',
  spacing: 20,
  padding: const EdgeInsets.all(32),
  textAlign: TextAlign.center,
  titleStyle: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
  messageStyle: TextStyle(color: Colors.grey.shade600),
  actionText: 'Browse items',
  onAction: _browse,
  buttonStyle: FilledButton.styleFrom(backgroundColor: Colors.pink),
)

Need a full illustration instead of a Material icon? Use iconWidget:

EmptyState(
  iconWidget: Image.asset('assets/empty_box.png', height: 140),
  title: 'Your cart is empty',
)

Global styling with EmptyStateTheme #

Don't want to repeat the same iconColor: / spacing: on every screen? Set your defaults once as a theme extension and every widget picks them up:

MaterialApp(
  theme: ThemeData(
    extensions: const [
      EmptyStateTheme(
        iconColor: Colors.teal,
        spacing: 20,
        titleStyle: TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
      ),
    ],
  ),
)

Resolution order for every property is explicit argument → EmptyStateTheme → built-in default, so a one-off EmptyState(iconColor: Colors.red) still wins where you need it.

Animation & accessibility #

Each widget plays a subtle fade-and-slide when it first appears, and StateView cross-fades between states. It's all opt-out:

EmptyState(animate: false);                                  // no entrance animation
EmptyState(animationDuration: const Duration(milliseconds: 200));
StateView(state: state, duration: Duration.zero, child: ...); // no cross-fade

Animations are skipped automatically when the user has "reduce motion" turned on. The loading state is also exposed as a live region with a semantic label, so screen readers announce the wait.

Light & dark themes #

By default every widget reads its colours and text styles from Theme.of(context):

  • the icon uses colorScheme.onSurfaceVariant,
  • the title uses textTheme.titleMedium,
  • the message uses textTheme.bodyMedium,
  • the button is a Material 3 FilledButton that follows your colorScheme.

So switching your app between light and dark needs no extra work here — these widgets follow along.

Placing it in a layout #

The widgets center themselves when they're given a bounded height (a Scaffold body, a Center, an Expanded) and lay out naturally inside a scrollable like ListView, so the same widget works in all of these:

Scaffold(body: const EmptyState());                 // centered on screen
Center(child: const EmptyState());                  // centered
Column(children: [Expanded(child: EmptyState())]);  // fills and centers
ListView(children: const [EmptyState()]);           // sits naturally, no crash

Why use this package? #

  • Consistency — the same empty/error/loading look across every screen and every project.
  • Less boilerplate — no more hand-rolled Center/Column/Text for each state.
  • No baggage — zero runtime dependencies and a small surface area.
  • Theme-first — light and dark are handled for you.
  • Customisable when it matters — good defaults now, full control later.

Running the tests #

flutter test                       # unit + widget tests
flutter test --exclude-tags golden # what CI runs (skips pixel tests)
flutter test --update-goldens --tags golden  # regenerate the golden images

Golden (pixel) tests are tagged golden and excluded on CI, since golden files are tied to the platform that generated them.

Contributing #

Issues and pull requests are welcome over on GitHub. CI runs formatting, analysis, tests and a publish dry-run on every push.

License #

MIT

1
likes
160
points
23
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Lightweight, theme-aware empty, error, no-internet, search-empty and loading state widgets for Flutter. Customizable and dependency-free.

Repository (GitHub)
View/report issues

Topics

#ui #widget #empty-state #placeholder #loading

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_empty_state