Web351 views

How to Build a Web3 Wallet Generation Loading Screen in Flutter (Full Code + Preview)

Real wallet key generation takes a few unpredictable seconds, and a blank spinner during that wait makes a crypto user nervous at the wrong moment. This tutorial builds Aurum's Generating Wallet screen in Flutter: a 168px painted ring with a gold progress arc, a blurred comet head and a live percentage drawn dead-centre, a subtitle that cross-fades through four key-generation phases, a checklist whose dots turn gold then green as each phase completes, and a painted shield badge assuring the reader keys never leave the device.

Aurum · Generating Wallet — Web3 Flutter UI screen
Live preview — Aurum · Generating Wallet, built in pure Flutter.

What you'll build

  • A CustomPaint spinner combining a grey track ring, a gold sweeping progress arc, an orbiting blurred comet head, and a TextPainter percentage in the centre
  • A 4.4-second progress AnimationController whose value drives the percent readout, the current phase index, and an onComplete handoff
  • An AnimatedSwitcher subtitle that cross-fades through 'Generating entropy…' to 'Securing your wallet…' as the step index changes
  • A four-row checklist where each dot moves from hairline grey to gold (active) to a green tinted circle with a check icon (done)
  • A tiny _ShieldTickPainter badge that draws a green shield and tick from Path curves, no icon font or SVG asset

Step-by-step build

1

Create the file

Add a new file at lib/web3_create_generating/web3_create_generating_screen.dart in your Flutter project.

2

Register the bundled fonts

No external packages — this is pure Flutter. It does bundle its design font (Inter), so drop the font file into fonts/ and declare it in pubspec.yaml:

pubspec.yaml
flutter:
  fonts:
    - family: Inter
      fonts:
        - asset: fonts/Inter-Regular.ttf
3

Build it, piece by piece

Here's how the screen goes together. Each block below is a real slice of the code with a plain-English explanation — paste them in order, or grab the whole file from the next section.

One callback, one font, one palette, four phases

web3_create_generating_screen.dart
import 'dart:math' as math;

import 'package:flutter/material.dart';

/// Web3 create — Generating Wallet. An animated painted ring spins while keys
/// "generate", with stepped progress copy. Self-contained per CONVENTIONS.md:
/// pure Flutter, bundled Inter font, painted spinner (no network image),
/// forced dark theme so it renders standalone as a route. After the simulated
/// generation completes it invokes [onComplete].
class Web3CreateGeneratingScreen extends StatefulWidget {
  const Web3CreateGeneratingScreen({super.key, this.onComplete});

  final VoidCallback? onComplete;

  @override
  State<Web3CreateGeneratingScreen> createState() =>
      _Web3CreateGeneratingScreenState();
}

class _Web3CreateGeneratingScreenState extends State<Web3CreateGeneratingScreen>
    with TickerProviderStateMixin {
  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF0B0E11);
  static const Color _brand = Color(0xFFF0B90B);
  static const Color _up = Color(0xFF2EBD85);
  static const Color _text = Color(0xFFEAECEF);
  static const Color _muted = Color(0xFF848E9C);
  static const Color _hairline = Color(0xFF2B3139);

  static const List<String> _steps = <String>[
    'Generating entropy…',
    'Deriving private keys…',
    'Creating recovery phrase…',
    'Securing your wallet…',
  ];

`Web3CreateGeneratingScreen` takes a single optional `VoidCallback? onComplete` — a loading screen has no taps to handle, so its only contract with the outside world is 'tell me when to move on'. The palette is Binance-flavoured: near-black `_bg` `#0B0E11`, gold `_brand` `#F0B90B` for progress, green `_up` `#2EBD85` reserved for completed states, and `_muted`/`_hairline` greys for secondary text and inactive dots. The four phase strings live in a `static const List<String> _steps`, which matters because the same list feeds both the cross-fading subtitle and the checklist — edit the copy once and both stay in sync.

Two controllers with different jobs

web3_create_generating_screen.dart

  late final AnimationController _spin;
  late final AnimationController _progress;
  int _step = 0;

  @override
  void initState() {
    super.initState();
    _spin = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 1400),
    )..repeat();
    _progress = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 4400),
    )..addStatusListener((AnimationStatus s) {
        if (s == AnimationStatus.completed) {
          widget.onComplete?.call();
        }
      });
    _progress.addListener(() {
      final int next = (_progress.value * _steps.length)
          .floor()
          .clamp(0, _steps.length - 1);
      if (next != _step) {
        setState(() => _step = next);
      }
    });
    _progress.forward();
  }

  @override
  void dispose() {
    _spin.dispose();
    _progress.dispose();
    super.dispose();
  }

The state mixes in `TickerProviderStateMixin` because it runs two `AnimationController`s at once: `_spin` repeats every 1400ms forever and only moves the comet head, while `_progress` runs a single 4400ms `forward()` and owns everything meaningful — the arc sweep, the percentage, and the step index. A status listener fires `widget.onComplete?.call()` when `_progress` completes, and a value listener derives the phase as `(_progress.value * _steps.length).floor().clamp(0, _steps.length - 1)`, calling `setState` only when the integer actually changes so the subtitle and checklist rebuild four times, not sixty times a second. Both controllers are disposed, in order, in `dispose()`.

The ring, the heading, and a cross-fading status line

web3_create_generating_screen.dart
  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.dark(useMaterial3: true),
      child: Scaffold(
        backgroundColor: _bg,
        body: SafeArea(
          child: Padding(
            padding: const EdgeInsets.fromLTRB(24, 24, 24, 32),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                const Spacer(),
                Center(
                  child: SizedBox(
                    width: 168,
                    height: 168,
                    child: AnimatedBuilder(
                      animation: Listenable.merge(<Listenable>[_spin, _progress]),
                      builder: (BuildContext context, Widget? child) {
                        return CustomPaint(
                          painter: _SpinnerPainter(
                            spin: _spin.value,
                            progress: _progress.value,
                          ),
                        );
                      },
                    ),
                  ),
                ),
                const SizedBox(height: 40),
                const Text(
                  'Creating your wallet',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontFamily: _font,
                    fontSize: 24,
                    fontWeight: FontWeight.w700,
                    letterSpacing: -0.3,
                    color: _text,
                  ),
                ),
                const SizedBox(height: 10),
                AnimatedSwitcher(
                  duration: const Duration(milliseconds: 250),
                  child: Text(
                    _steps[_step],
                    key: ValueKey<int>(_step),
                    textAlign: TextAlign.center,
                    style: const TextStyle(
                      fontFamily: _font,
                      fontSize: 14.5,
                      color: _muted,
                    ),
                  ),
                ),

`build` wraps the `Scaffold` in `Theme(data: ThemeData.dark(useMaterial3: true))` so the screen renders correctly as a standalone route regardless of the app's theme. The spinner is a fixed 168×168 `SizedBox` around a `CustomPaint`, rebuilt by an `AnimatedBuilder` listening to `Listenable.merge([_spin, _progress])` — one builder subscribed to both controllers, rather than nesting two. Below it, the 24px `w700` heading stays constant while an `AnimatedSwitcher` with a 250ms duration cross-fades the subtitle; the `key: ValueKey<int>(_step)` is what tells the switcher the text is a *new* child worth animating, since the widget type alone never changes.

The checklist and the security footer

web3_create_generating_screen.dart
                const Spacer(),
                Column(
                  children: <Widget>[
                    for (int i = 0; i < _steps.length; i++)
                      _StepRow(
                        label: _steps[i],
                        done: i < _step,
                        active: i == _step,
                      ),
                  ],
                ),
                const SizedBox(height: 24),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    SizedBox(
                      width: 16,
                      height: 16,
                      child: CustomPaint(painter: _ShieldTickPainter()),
                    ),
                    const SizedBox(width: 8),
                    const Text(
                      'Keys generated securely on this device',
                      style: TextStyle(
                        fontFamily: _font,
                        fontSize: 12.5,
                        color: _muted,
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

A `Spacer` above and below the ring group centres it vertically, pushing the checklist and footer to the bottom. The checklist is a collection-for over `_steps`, and each `_StepRow` is told its state relationally: `done: i < _step`, `active: i == _step` — no per-row booleans to maintain, the single `_step` integer positions all four rows. The footer row pairs a 16px `CustomPaint` running `_ShieldTickPainter` with the 12.5px muted line 'Keys generated securely on this device', which is the one sentence a non-custodial wallet most needs its user to read during this wait.

_StepRow: three states from two booleans

web3_create_generating_screen.dart
class _StepRow extends StatelessWidget {
  const _StepRow({required this.label, required this.done, required this.active});

  final String label;
  final bool done;
  final bool active;

  @override
  Widget build(BuildContext context) {
    final Color dot = done
        ? _Web3CreateGeneratingScreenState._up
        : active
            ? _Web3CreateGeneratingScreenState._brand
            : _Web3CreateGeneratingScreenState._hairline;
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 6),
      child: Row(
        children: <Widget>[
          Container(
            width: 18,
            height: 18,
            decoration: BoxDecoration(
              color: done
                  ? _Web3CreateGeneratingScreenState._up.withValues(alpha: 0.16)
                  : Colors.transparent,
              shape: BoxShape.circle,
              border: Border.all(color: dot, width: 2),
            ),
            child: done
                ? const Icon(Icons.check_rounded,
                    size: 11, color: _Web3CreateGeneratingScreenState._up)
                : null,
          ),
          const SizedBox(width: 12),
          Text(
            label,
            style: TextStyle(
              fontFamily: _Web3CreateGeneratingScreenState._font,
              fontSize: 13.5,
              fontWeight: active ? FontWeight.w600 : FontWeight.w400,
              color: done || active
                  ? _Web3CreateGeneratingScreenState._text
                  : _Web3CreateGeneratingScreenState._muted,
            ),
          ),
        ],
      ),
    );
  }
}

Each row resolves its dot colour with a nested ternary — green `_up` when done, gold `_brand` when active, `_hairline` grey otherwise — and the done state additionally fills the 18px circle with `_up.withValues(alpha: 0.16)` and drops in an 11px `Icons.check_rounded`, so completion reads as a filled badge rather than just a recolour. The label mirrors the same hierarchy in typography: `FontWeight.w600` only while active, and full `_text` colour for done-or-active rows against `_muted` for the ones still pending. Private classes reaching into `_Web3CreateGeneratingScreenState`'s static constants is what keeps the whole screen on one palette without a theme extension.

_SpinnerPainter: track, arc, comet, percentage

web3_create_generating_screen.dart
class _SpinnerPainter extends CustomPainter {
  const _SpinnerPainter({required this.spin, required this.progress});

  final double spin;
  final double progress;

  @override
  void paint(Canvas canvas, Size size) {
    final Offset c = Offset(size.width / 2, size.height / 2);
    final double r = size.width / 2 - 8;
    final Rect rect = Rect.fromCircle(center: c, radius: r);

    // Track.
    canvas.drawCircle(
      c,
      r,
      Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 8
        ..color = const Color(0xFF2B3139),
    );
    // Progress arc.
    canvas.drawArc(
      rect,
      -math.pi / 2,
      2 * math.pi * progress,
      false,
      Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 8
        ..strokeCap = StrokeCap.round
        ..color = const Color(0xFFF0B90B),
    );
    // Spinning comet head.
    final double a = spin * 2 * math.pi - math.pi / 2;
    final Offset head = c + Offset(math.cos(a) * r, math.sin(a) * r);
    canvas.drawCircle(
      head,
      5,
      Paint()
        ..color = const Color(0xFFF7D14B)
        ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2),
    );

    // Center percentage.
    final TextPainter tp = TextPainter(
      text: TextSpan(
        text: '${(progress * 100).round()}%',
        style: const TextStyle(
          fontFamily: 'Inter',
          fontSize: 30,
          fontWeight: FontWeight.w700,
          color: Color(0xFFEAECEF),
        ),
      ),
      textDirection: TextDirection.ltr,
    )..layout();
    tp.paint(canvas, c - Offset(tp.width / 2, tp.height / 2));
  }

  @override
  bool shouldRepaint(_SpinnerPainter oldDelegate) =>
      oldDelegate.spin != spin || oldDelegate.progress != progress;
}

The painter draws in four passes at radius `size.width / 2 - 8`, insetting so the 8px stroke never clips. The grey track is a full `drawCircle`; the gold arc is `drawArc` starting at `-math.pi / 2` (twelve o'clock) sweeping `2 * math.pi * progress` with `StrokeCap.round` so the arc ends in a soft tip. The comet head converts `_spin`'s 0–1 value to an angle and places a 5px `#F7D14B` circle on the ring via `cos`/`sin`, blurred with `MaskFilter.blur(BlurStyle.normal, 2)` to glow rather than sit as a hard dot. The percentage is a `TextPainter` laid out and painted at `c - Offset(tp.width / 2, tp.height / 2)` — dead centre without a `Stack`, because the painter already knows the canvas geometry. `shouldRepaint` compares both `spin` and `progress` so the ring repaints only when either driver actually moved.

_ShieldTickPainter: a badge from six path segments

web3_create_generating_screen.dart
class _ShieldTickPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final double w = size.width, h = size.height;
    final Path shield = Path()
      ..moveTo(w / 2, 0)
      ..lineTo(w, h * 0.22)
      ..lineTo(w, h * 0.55)
      ..quadraticBezierTo(w, h * 0.9, w / 2, h)
      ..quadraticBezierTo(0, h * 0.9, 0, h * 0.55)
      ..lineTo(0, h * 0.22)
      ..close();
    canvas.drawPath(shield, Paint()..color = const Color(0xFF2EBD85));
    final Paint tick = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1.6
      ..strokeCap = StrokeCap.round
      ..color = const Color(0xFF0B0E11);
    canvas.drawPath(
      Path()
        ..moveTo(w * 0.3, h * 0.5)
        ..lineTo(w * 0.45, h * 0.64)
        ..lineTo(w * 0.72, h * 0.36),
      tick,
    );
  }

  @override
  bool shouldRepaint(_ShieldTickPainter oldDelegate) => false;
}

The shield is one `Path`: straight lines form the top edge and sides down to 55% height, then two `quadraticBezierTo` curves taper the sides into the bottom point, giving the classic shield silhouette without any asset. It is filled solid `_up` green, and the tick is a second path stroked at 1.6px in the background colour `#0B0E11` — cutting the mark out visually instead of drawing white, so it stays consistent if the badge sits on the dark scaffold. Every coordinate is a fraction of `size`, so the same painter works at the footer's 16px or ten times that, and `shouldRepaint` returns `false` because nothing here depends on state.

Full code

The complete, ready-to-paste source. Free to use in your projects — one click copies it all.

import 'dart:math' as math;

import 'package:flutter/material.dart';

/// Web3 create — Generating Wallet. An animated painted ring spins while keys
/// "generate", with stepped progress copy. Self-contained per CONVENTIONS.md:
/// pure Flutter, bundled Inter font, painted spinner (no network image),
/// forced dark theme so it renders standalone as a route. After the simulated
/// generation completes it invokes [onComplete].
class Web3CreateGeneratingScreen extends StatefulWidget {
  const Web3CreateGeneratingScreen({super.key, this.onComplete});

  final VoidCallback? onComplete;

  @override
  State<Web3CreateGeneratingScreen> createState() =>
      _Web3CreateGeneratingScreenState();
}

class _Web3CreateGeneratingScreenState extends State<Web3CreateGeneratingScreen>
    with TickerProviderStateMixin {
  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF0B0E11);
  static const Color _brand = Color(0xFFF0B90B);
  static const Color _up = Color(0xFF2EBD85);
  static const Color _text = Color(0xFFEAECEF);
  static const Color _muted = Color(0xFF848E9C);
  static const Color _hairline = Color(0xFF2B3139);

  static const List<String> _steps = <String>[
    'Generating entropy…',
    'Deriving private keys…',
    'Creating recovery phrase…',
    'Securing your wallet…',
  ];

  late final AnimationController _spin;
  late final AnimationController _progress;
  int _step = 0;

  @override
  void initState() {
    super.initState();
    _spin = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 1400),
    )..repeat();
    _progress = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 4400),
    )..addStatusListener((AnimationStatus s) {
        if (s == AnimationStatus.completed) {
          widget.onComplete?.call();
        }
      });
    _progress.addListener(() {
      final int next = (_progress.value * _steps.length)
          .floor()
          .clamp(0, _steps.length - 1);
      if (next != _step) {
        setState(() => _step = next);
      }
    });
    _progress.forward();
  }

  @override
  void dispose() {
    _spin.dispose();
    _progress.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.dark(useMaterial3: true),
      child: Scaffold(
        backgroundColor: _bg,
        body: SafeArea(
          child: Padding(
            padding: const EdgeInsets.fromLTRB(24, 24, 24, 32),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                const Spacer(),
                Center(
                  child: SizedBox(
                    width: 168,
                    height: 168,
                    child: AnimatedBuilder(
                      animation: Listenable.merge(<Listenable>[_spin, _progress]),
                      builder: (BuildContext context, Widget? child) {
                        return CustomPaint(
                          painter: _SpinnerPainter(
                            spin: _spin.value,
                            progress: _progress.value,
                          ),
                        );
                      },
                    ),
                  ),
                ),
                const SizedBox(height: 40),
                const Text(
                  'Creating your wallet',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontFamily: _font,
                    fontSize: 24,
                    fontWeight: FontWeight.w700,
                    letterSpacing: -0.3,
                    color: _text,
                  ),
                ),
                const SizedBox(height: 10),
                AnimatedSwitcher(
                  duration: const Duration(milliseconds: 250),
                  child: Text(
                    _steps[_step],
                    key: ValueKey<int>(_step),
                    textAlign: TextAlign.center,
                    style: const TextStyle(
                      fontFamily: _font,
                      fontSize: 14.5,
                      color: _muted,
                    ),
                  ),
                ),
                const Spacer(),
                Column(
                  children: <Widget>[
                    for (int i = 0; i < _steps.length; i++)
                      _StepRow(
                        label: _steps[i],
                        done: i < _step,
                        active: i == _step,
                      ),
                  ],
                ),
                const SizedBox(height: 24),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    SizedBox(
                      width: 16,
                      height: 16,
                      child: CustomPaint(painter: _ShieldTickPainter()),
                    ),
                    const SizedBox(width: 8),
                    const Text(
                      'Keys generated securely on this device',
                      style: TextStyle(
                        fontFamily: _font,
                        fontSize: 12.5,
                        color: _muted,
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class _StepRow extends StatelessWidget {
  const _StepRow({required this.label, required this.done, required this.active});

  final String label;
  final bool done;
  final bool active;

  @override
  Widget build(BuildContext context) {
    final Color dot = done
        ? _Web3CreateGeneratingScreenState._up
        : active
            ? _Web3CreateGeneratingScreenState._brand
            : _Web3CreateGeneratingScreenState._hairline;
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 6),
      child: Row(
        children: <Widget>[
          Container(
            width: 18,
            height: 18,
            decoration: BoxDecoration(
              color: done
                  ? _Web3CreateGeneratingScreenState._up.withValues(alpha: 0.16)
                  : Colors.transparent,
              shape: BoxShape.circle,
              border: Border.all(color: dot, width: 2),
            ),
            child: done
                ? const Icon(Icons.check_rounded,
                    size: 11, color: _Web3CreateGeneratingScreenState._up)
                : null,
          ),
          const SizedBox(width: 12),
          Text(
            label,
            style: TextStyle(
              fontFamily: _Web3CreateGeneratingScreenState._font,
              fontSize: 13.5,
              fontWeight: active ? FontWeight.w600 : FontWeight.w400,
              color: done || active
                  ? _Web3CreateGeneratingScreenState._text
                  : _Web3CreateGeneratingScreenState._muted,
            ),
          ),
        ],
      ),
    );
  }
}

class _SpinnerPainter extends CustomPainter {
  const _SpinnerPainter({required this.spin, required this.progress});

  final double spin;
  final double progress;

  @override
  void paint(Canvas canvas, Size size) {
    final Offset c = Offset(size.width / 2, size.height / 2);
    final double r = size.width / 2 - 8;
    final Rect rect = Rect.fromCircle(center: c, radius: r);

    // Track.
    canvas.drawCircle(
      c,
      r,
      Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 8
        ..color = const Color(0xFF2B3139),
    );
    // Progress arc.
    canvas.drawArc(
      rect,
      -math.pi / 2,
      2 * math.pi * progress,
      false,
      Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 8
        ..strokeCap = StrokeCap.round
        ..color = const Color(0xFFF0B90B),
    );
    // Spinning comet head.
    final double a = spin * 2 * math.pi - math.pi / 2;
    final Offset head = c + Offset(math.cos(a) * r, math.sin(a) * r);
    canvas.drawCircle(
      head,
      5,
      Paint()
        ..color = const Color(0xFFF7D14B)
        ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2),
    );

    // Center percentage.
    final TextPainter tp = TextPainter(
      text: TextSpan(
        text: '${(progress * 100).round()}%',
        style: const TextStyle(
          fontFamily: 'Inter',
          fontSize: 30,
          fontWeight: FontWeight.w700,
          color: Color(0xFFEAECEF),
        ),
      ),
      textDirection: TextDirection.ltr,
    )..layout();
    tp.paint(canvas, c - Offset(tp.width / 2, tp.height / 2));
  }

  @override
  bool shouldRepaint(_SpinnerPainter oldDelegate) =>
      oldDelegate.spin != spin || oldDelegate.progress != progress;
}

class _ShieldTickPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final double w = size.width, h = size.height;
    final Path shield = Path()
      ..moveTo(w / 2, 0)
      ..lineTo(w, h * 0.22)
      ..lineTo(w, h * 0.55)
      ..quadraticBezierTo(w, h * 0.9, w / 2, h)
      ..quadraticBezierTo(0, h * 0.9, 0, h * 0.55)
      ..lineTo(0, h * 0.22)
      ..close();
    canvas.drawPath(shield, Paint()..color = const Color(0xFF2EBD85));
    final Paint tick = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1.6
      ..strokeCap = StrokeCap.round
      ..color = const Color(0xFF0B0E11);
    canvas.drawPath(
      Path()
        ..moveTo(w * 0.3, h * 0.5)
        ..lineTo(w * 0.45, h * 0.64)
        ..lineTo(w * 0.72, h * 0.36),
      tick,
    );
  }

  @override
  bool shouldRepaint(_ShieldTickPainter oldDelegate) => false;
}

Plus bundled 1 binary asset (fonts/images). The CLI and MCP install those for you automatically.

Two faster ways to add it

Copy-paste works, but you can skip it entirely.

1. FlutterKit CLI

One command drops this screen — and its fonts — straight into your project.

$ flutterkit add web3-create-generating

2. AI agent (MCP)

Connect FlutterKit's MCP server in Claude or Cursor and just ask your agent to install web3-create-generating — it fetches and writes the files for you.

FAQ

Is this wallet generation screen free to use commercially?

Yes. FlutterKit screens are free, commercial use included — ship this loader in a production wallet or exchange app with no licence fee, sign-up, or attribution. Copy the Dart from this page or install it with the CLI command shown above.

Does this screen need any packages, images, or font files?

No packages at all — the imports are `dart:math` and `package:flutter/material.dart`. The spinner, comet, and shield badge are all CustomPainter output, so there is no network image or SVG, and the Inter font referenced by `_font` ships bundled with the screen's assets rather than being fetched at runtime.

Which Flutter version does this need?

Flutter 3.27 or newer, because `_StepRow` tints its done circle with `_up.withValues(alpha: 0.16)`. On an older SDK, swap that call for `withOpacity(0.16)`; the `super.key` constructor also assumes Flutter 3.0+ / Dart 2.17+, which any current project already meets.

How do I replace the 4.4-second timer with real key generation?

Keep `_spin` as-is and stop calling `_progress.forward()` in `initState`. Instead, drive `_progress.animateTo(fraction)` from your key-gen pipeline — for example 0.25 after entropy, 0.5 after key derivation, 0.75 after the recovery phrase, 1.0 once storage is secured. The existing value listener will keep the subtitle and checklist in step, and the status listener still fires `onComplete` when the value reaches 1.0.

Why is the percentage painted inside the CustomPainter instead of a Text widget in a Stack?

Because the painter already knows the exact canvas centre. The `TextPainter` is laid out and painted at `c - Offset(tp.width / 2, tp.height / 2)`, so the number is pixel-centred in the ring with no `Stack`, `Center`, or alignment tuning — and it repaints in the same pass as the arc it describes, so the two can never be a frame apart.

Related screens