Skip to main content

No project description provided

Project description

Flet Splash

CLI tool that automatically injects fully customizable splash screens into Flet apps during the Flutter build process. Configure once in pyproject.toml, build with fs-build apk, and your app launches with a beautiful custom splash.

Python Flet License


Solves flet-dev/flet#5523 — Unified and fully customizable startup sequence (splash → boot → startup) with support for custom Flutter widgets/animations.

The default Flet startup experience shows a blank white screen → a CircularProgressIndicator → then finally the app. This creates a jarring, unprofessional launch experience. flet-splash replaces the entire startup sequence with a smooth, customizable splash screen that covers all loading phases and fades out gracefully when the app is ready.


Table of Contents


Buy Me a Coffee

If you find this project useful, please consider supporting its development:

Buy Me a Coffee

The Problem

When a Flet app starts, users see three different screens before the actual app appears:

1. BlankScreen (white)  →  2. LoadingPage (spinner)  →  3. Your app

This creates a flickering, unprofessional startup experience — especially on mobile where cold starts can take several seconds.

The Solution

flet-splash injects a custom splash overlay that covers all three phases with a single, smooth screen:

1. CustomSplash (your design)  →  fade out  →  Your app

The splash stays visible throughout the entire boot process and fades out gracefully once the app is ready. No flicker, no spinner — just your brand.


Installation

# Using UV (recommended)
uv add flet-splash

# Using pip
pip install flet-splash

# From GitHub (latest development version)
uv add flet-splash@git+https://github.com/brunobrown/flet-splash.git

# or

pip install git+https://github.com/brunobrown/flet-splash.git

Requirements: Python 3.10+, Flet 0.80.0+


Quick Start

1. Configure your splash in pyproject.toml:

[tool.flet.splash]
type = "color"
background = "#1a1a2e"
min_duration = 5.0

2. Build your app:

fs-build apk

That's it. The splash screen is automatically injected into the Flutter build.


How It Works

flet-splash uses a multi-pass build strategy:

Step 1/3  First pass — flet build generates the Flutter project
Step 2/3  Inject — patches main.dart, pubspec.yaml, and copies assets
Step 3/3  Rebuild — flet build recompiles with the splash injected

The injection is:

  • Automatic — no manual Flutter/Dart editing required
  • Idempotent — running twice won't double-inject (marker-based detection)
  • Non-destructive — only modifies BlankScreen, runApp, and pubspec.yaml
  • Smart — if the Flutter project already exists, skips the first pass

What gets patched

File Change
lib/main.dart BlankScreen class → CustomSplash (your design)
lib/main.dart runApp(FutureBuilder(...))runApp(_SplashBootstrap(child: FutureBuilder(...)))
lib/main.dart _SplashBootstrap overlay with AnimatedOpacity appended
pubspec.yaml Dependencies added (lottie, flutter_svg) if needed
pubspec.yaml Asset entries added to flutter.assets
splash_assets/ Source file copied (image, lottie, svg)

Build Process in Detail

This section explains exactly what happens when you run fs-build apk — from reading your config to delivering the final APK.

1. Configuration Loading

flet-splash reads your pyproject.toml and merges it with any CLI flags:

pyproject.toml [tool.flet.splash]  ←  defaults
         ↓
    CLI flags override             ←  --type, --source, --background, etc.
         ↓
    SplashConfig (final)           ←  validated, ready to use

Validations at this stage:

  • Types lottie, image, svg, and custom require a source file
  • Type custom requires a .dart extension
  • The source file must exist on disk

2. Build Orchestration

flet-splash detects the current state and chooses the optimal build path:

                          ┌─────────────────────────────┐
                          │  Does build/flutter/ exist? │
                          └──────────────┬──────────────┘
                                         │
                          ┌──── NO ──────┼────── YES ───┐
                          │              │              │
                          ▼              │              ▼
                   ┌─────────────┐       │    ┌────────────────────┐
                   │  FULL BUILD │       │    │  Splash injected?  │
                   │  (3 steps)  │       │    └─────────┬──────────┘
                   └─────────────┘       │         YES  │  NO
                                         │          │   │
                                         │          ▼   ▼
                                         │    ┌──────┐ ┌──────────────┐
                                         │    │SINGLE│ │INJECT+REBUILD│
                                         │    │ PASS │ │  (2 steps)   │
                                         │    └──────┘ └──────────────┘

Scenario A — Full Build (first time):

Step 1/3  flet build apk         →  generates build/flutter/ (the Flutter project)
Step 2/3  inject_splash()        →  patches main.dart, pubspec.yaml, copies assets
Step 3/3  flet build apk         →  recompiles with splash injected

Scenario B — Inject + Rebuild (Flutter project exists but no splash):

Step 1/2  inject_splash()        →  patches existing Flutter project
Step 2/2  flet build apk         →  compiles with splash

Scenario C — Single Pass (splash already injected):

Step 1/1  flet build apk         →  builds directly (nothing to inject)

3. Injection: main.dart Patching

The injection modifies build/flutter/lib/main.dart in 5 sequential patches:

Patch 1 — Add Imports

If the splash type requires external packages, the corresponding imports are added after the last existing import statement:

// BEFORE (original Flet template)
import 'package:flet/flet.dart';
import 'package:flutter/material.dart';

// AFTER (lottie type)
import 'package:flet/flet.dart';
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';        // ← added

// AFTER (svg type)
import 'package:flet/flet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; // ← added

Types color, image, and custom don't add any imports.

Patch 2 — Replace BlankScreen Class

The original BlankScreen class (a blank Scaffold) is entirely replaced by CustomSplash — your splash widget. The replacement uses brace-depth counting to accurately find the class boundaries, regardless of inner classes or nested braces:

// BEFORE (original Flet template)
class BlankScreen extends StatelessWidget {
  const BlankScreen({super.key});
  @override
  Widget build(BuildContext context) {
    return const Scaffold(body: SizedBox.shrink());
  }
}

// AFTER (replaced by flet-splash — example with image type)
// [flet-splash] Custom splash screen
class CustomSplash extends StatelessWidget {
  const CustomSplash({super.key});
  @override
  Widget build(BuildContext context) {
    var brightness = WidgetsBinding.instance.platformDispatcher.platformBrightness;
    return ColoredBox(
      color: brightness == Brightness.dark
          ? const Color(0xFF0a0a1e)
          : const Color(0xFF1a1a2e),
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Image.asset('splash_assets/custom_splash.png'),
          ],
        ),
      ),
    );
  }
}

For type = "custom", the entire content of your .dart file replaces the class.

Patch 3 — Replace BlankScreen References

All BlankScreen() constructor calls in the code are replaced with CustomSplash():

// BEFORE
return const MaterialApp(home: BlankScreen());

// AFTER
return const MaterialApp(home: CustomSplash());

Patch 4 — Wrap runApp with _SplashBootstrap

The runApp call is wrapped to add the splash overlay on top of the entire widget tree:

// BEFORE
runApp(FutureBuilder(
    future: prepareApp(),
    builder: (BuildContext context, AsyncSnapshot snapshot) {
        // ... app content ...
    }));

// AFTER
runApp(_SplashBootstrap(child: FutureBuilder(
    future: prepareApp(),
    builder: (BuildContext context, AsyncSnapshot snapshot) {
        // ... app content ...
    })));

This ensures the splash overlay sits above everythingBlankScreen, LoadingPage, and the app itself.

Patch 5 — Append _SplashBootstrap Class

The _SplashBootstrap widget is appended at the end of main.dart. This is the core mechanism that creates the overlay effect:

A global ValueNotifier is also added to main.dart:

final ValueNotifier<bool> _appReady = ValueNotifier(false);

And inside the FutureBuilder, when snapshot.hasData (meaning prepareApp() completed and FletApp is about to render):

if (snapshot.hasData) {
  _appReady.value = true;  // ← signals the bootstrap
  return FletApp(...);
}

The _SplashBootstrap widget listens to both conditions:

class _SplashBootstrap extends StatefulWidget {
  final Widget child;
  const _SplashBootstrap({required this.child});

  @override
  State<_SplashBootstrap> createState() => _SplashBootstrapState();
}

class _SplashBootstrapState extends State<_SplashBootstrap> {
  bool _showSplash = true;
  bool _timerDone = false;

  @override
  void initState() {
    super.initState();
    Future.delayed(const Duration(milliseconds: 5000), () {  // ← min_duration
      _timerDone = true;
      _maybeHide();
    });
    _appReady.addListener(_maybeHide);  // ← listens for app readiness
  }

  void _maybeHide() {
    // Only fade when BOTH conditions are met
    if (_timerDone && _appReady.value && mounted) {
      setState(() => _showSplash = false);
    }
  }

  @override
  void dispose() {
    _appReady.removeListener(_maybeHide);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Directionality(
      textDirection: TextDirection.ltr,
      child: Stack(
        children: [
          widget.child,                    // ← the actual app (behind)
          IgnorePointer(
            ignoring: !_showSplash,        // ← lets taps pass through during fade
            child: AnimatedOpacity(
              opacity: _showSplash ? 1.0 : 0.0,
              duration: const Duration(milliseconds: 500),  // ← fade_duration
              child: const CustomSplash(), // ← your splash (on top)
            ),
          ),
        ],
      ),
    );
  }
}

How it works at runtime:

  1. App starts → _SplashBootstrap renders a Stack with the app behind and CustomSplash on top (opacity 1.0)
  2. The app boots normally underneath (invisible to the user)
  3. min_duration timer runs in parallel with app initialization
  4. When prepareApp() completes → _appReady.value = true
  5. The splash fades only when both: timer elapsed AND app is ready
  6. AnimatedOpacity fades from 1.0 to 0.0 over fade_duration ms
  7. IgnorePointer allows touch events to pass through during the fade
  8. The splash becomes fully transparent → the app is revealed

This dual-condition approach ensures no white screen flash on cold starts — the splash stays visible until the app is actually ready to display, regardless of how long the first boot takes.

4. Injection: pubspec.yaml Patching

Dependencies and assets are added to build/flutter/pubspec.yaml:

# Dependencies added (only when needed):
dependencies:
  lottie: ^3.2.0             # ← added for type "lottie"
  flutter_svg: ^2.0.17       # ← added for type "svg"

# Asset entry appended to existing list:
flutter:
  assets:
    - app/app.zip             # ← existing Flet asset (preserved)
    - app/app.zip.hash        # ← existing Flet asset (preserved)
    - splash_assets/custom_splash.json  # ← added by flet-splash

The asset is appended at the end of the existing assets: list, preserving the original indentation.

5. Asset Copy

The source file is copied from your project into the Flutter build directory:

your_project/assets/custom_splash.json  →  build/flutter/splash_assets/custom_splash.json
your_project/assets/custom_splash.png   →  build/flutter/splash_assets/custom_splash.png
your_project/assets/custom_splash.svg   →  build/flutter/splash_assets/custom_splash.svg

For type = "color", no asset is copied. For type = "custom", the .dart content is injected directly into main.dart (no asset copy needed).

6. Rebuild

Finally, flet build runs again. This time the Flutter project already contains:

  • The CustomSplash widget (replacing BlankScreen)
  • The _SplashBootstrap overlay wrapping runApp
  • Any required dependencies (lottie, flutter_svg)
  • The splash asset file in splash_assets/

Flutter compiles everything into the final binary (APK, IPA, etc.) with the custom splash built-in.

Summary: File Flow

your_project/
├── pyproject.toml                     ← [1] config read from here
├── assets/
│   └── custom_splash.json             ← [5] copied to build/flutter/splash_assets/
└── build/
    └── flutter/                       ← generated by flet build (Step 1)
        ├── lib/
        │   └── main.dart              ← [3] patched (5 sequential modifications)
        ├── pubspec.yaml               ← [4] patched (deps + assets)
        └── splash_assets/
            └── custom_splash.json     ← [5] asset copied here

Configuration

pyproject.toml

All configuration goes under [tool.flet.splash]:

[tool.flet.splash]
type = "lottie"                      # lottie | image | svg | color | custom
source = "assets/custom_splash.json" # path to asset file (relative to project root)
background = "#1a1a2e"               # background color (hex)
dark_background = "#0a0a1e"          # dark mode background (optional, falls back to background)
min_duration = 5.0                   # minimum splash duration in seconds (see note below)
fade_duration = 0.5                  # fade-out animation duration in seconds
text = "Loading..."                  # optional text below the splash
text_color = "#ffffff"               # text color (hex)
text_size = 14                       # text font size in pixels

CLI Overrides

Any config option can be overridden via CLI flags:

fs-build apk --type lottie --source assets/custom_splash.json --background "#1a1a2e"
fs-build apk --min-duration 3.0 --fade-duration 0.8
fs-build apk --text "Loading..." --text-color "#cccccc" --text-size 16

Priority: CLI flags > pyproject.toml > defaults

All extra flags are passed directly to flet build:

# These flags go straight to flet build
fs-build apk -v --org com.example --build-version 1.0.0 --split-per-abi

Splash Types

Color

A solid color background. Simplest option — no external assets needed.

[tool.flet.splash]
type = "color"
background = "#1a1a2e"
dark_background = "#0d0d1a"

Image

A static image (PNG, JPG, GIF, WebP) centered on the splash screen.

[tool.flet.splash]
type = "image"
source = "assets/custom_splash.png"
background = "#0d47a1"

Lottie

A Lottie animation (JSON) that plays during startup. Great for animated logos and branded loading screens.

[tool.flet.splash]
type = "lottie"
source = "assets/custom_splash.json"
background = "#1b0536"
min_duration = 3.0

Tip: Download free Lottie animations from LottieFiles.

SVG

A vector graphic (SVG) rendered via flutter_svg. Ideal for logos that need to be crisp at any resolution.

[tool.flet.splash]
type = "svg"
source = "assets/custom_splash.svg"
background = "#1b1b2f"

Important: Do not name your SVG file splash.svg inside the assets/ folder. Flet automatically detects assets/splash.* files and passes them to flutter_native_splash, which does not support SVG format. Use a different name like custom_splash.svg.

Custom Dart Widget

For full control, provide your own .dart file with a CustomSplash widget. You can use any Flutter widget, animation, or layout.

[tool.flet.splash]
type = "custom"
source = "custom_splash.dart"
min_duration = 3.0

The .dart file must define a CustomSplash class that extends StatelessWidget or StatefulWidget:

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

  @override
  Widget build(BuildContext context) {
    return ColoredBox(
      color: const Color(0xFF1a1a2e),
      child: Center(
        child: TweenAnimationBuilder<double>(
          tween: Tween(begin: 0, end: 2 * 3.14159),
          duration: const Duration(seconds: 2),
          builder: (context, value, child) {
            return Transform.rotate(angle: value, child: child);
          },
          child: const Icon(Icons.rocket_launch, size: 64, color: Colors.white),
        ),
      ),
    );
  }
}

Note: When using type = "custom", the background and dark_background settings are ignored — your widget controls everything.


Text Overlay

Add optional text below the splash content (available for all types except custom):

[tool.flet.splash]
type = "image"
source = "assets/custom_splash.png"
background = "#1a1a2e"
text = "Loading..."
text_color = "#cccccc"
text_size = 16

The text is rendered as a Flutter Text widget positioned below the splash body.


Dark Mode Support

flet-splash automatically detects the device's brightness setting and applies the appropriate background:

[tool.flet.splash]
background = "#1a1a2e"           # light mode
dark_background = "#0a0a1e"      # dark mode (optional)

If dark_background is not set, the background color is used for both modes.


Understanding min_duration

The min_duration setting controls the minimum time the splash screen stays visible. It works together with the app readiness signal to determine when the splash fades out:

splash_visible_time = max(min_duration, app_initialization_time)

The splash fades only when both conditions are met:

  1. The min_duration timer has elapsed
  2. The app has finished initializing (prepareApp() completed)

This means:

  • If min_duration is longer than the initialization time → splash stays for the full min_duration
  • If initialization takes longer than min_duration → splash waits until the app is ready

Recommended values

On cold start (first launch after install), Flet apps go through several initialization steps: extracting app.zip, initializing the Python runtime, and connecting the WebSocket. This can take 5-8 seconds depending on the device.

On warm start (subsequent launches), everything is cached and the app starts much faster (~1-2 seconds).

To ensure the splash covers the entire cold start without showing a white screen, set min_duration to cover the worst case:

[tool.flet.splash]
min_duration = 5.0    # 5 seconds — covers most cold starts
Scenario Recommended min_duration
Simple app, fast devices 3.0 - 5.0
Complex app, varied devices 5.0 - 8.0
Lottie animation (match duration) Match your animation length

Why not min_duration = 0? The readiness signal (prepareApp() completed) fires before the Flet app has fully rendered its first frame. On cold start, there is a ~3 second gap between prepareApp() completing and the app content appearing on screen. Setting min_duration = 0 would cause the splash to fade during this gap, revealing a white screen. A higher min_duration ensures the splash covers this transition.


CLI Reference

Usage: fs-build [-h] [--type {lottie,image,svg,color,custom}]
                [--source SOURCE] [--background BACKGROUND]
                [--dark-background DARK_BACKGROUND]
                [--min-duration MIN_DURATION]
                [--fade-duration FADE_DURATION]
                [--text TEXT] [--text-color TEXT_COLOR]
                [--text-size TEXT_SIZE] [--clean]
                {apk,aab,ipa,web,macos,linux,windows}
Option Type Description
platform positional Target platform: apk, aab, ipa, web, macos, linux, windows
--type TEXT Splash type: lottie, image, svg, color, custom
--source PATH Path to splash asset file
--background TEXT Background color (hex, e.g. "#1a1a2e")
--dark-background TEXT Dark mode background color (hex)
--min-duration FLOAT Minimum splash duration in seconds
--fade-duration FLOAT Fade-out animation duration in seconds
--text TEXT Optional text below the splash
--text-color TEXT Text color (hex)
--text-size INT Text font size in pixels
--clean FLAG Clean build directory before building

All unrecognized flags are forwarded to flet build:

fs-build apk -v --org com.example --build-version 2.0.0
#                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#                these go directly to flet build

Important Notes

Asset naming

Flet automatically detects files named assets/splash.* and uses them as the native splash screen (via flutter_native_splash). To avoid conflicts:

  • Do not name your splash asset splash.png, splash.svg, splash.json, etc.
  • Use a different name like custom_splash.png, brand_logo.svg, loading.json, etc.

Build directory

flet-splash modifies files inside build/flutter/. If you encounter issues, use --clean to start fresh:

fs-build apk --clean

Idempotency

The injection is idempotent. If flet-splash detects its marker (// [flet-splash] Custom splash screen) in main.dart, it skips the injection step and proceeds directly to the build.


Examples

The examples/ directory contains ready-to-build sample apps for each splash type:

Example Type Description
color_splash color Solid color background — simplest configuration
image_splash image Static PNG image centered on splash
lottie_splash lottie Lottie JSON animation during startup
svg_splash svg SVG vector graphic via flutter_svg
custom_splash custom Custom Dart widget with rotation animation

To test an example:

cd examples/color_splash
fs-build apk

Supported Platforms

flet-splash works with all platforms supported by Flet:

Platform Command Notes
Android (APK) fs-build apk Debug APK
Android (AAB) fs-build aab Play Store bundle
iOS fs-build ipa Requires macOS + Xcode
Web fs-build web Static web app
macOS fs-build macos Desktop app
Linux fs-build linux Desktop app
Windows fs-build windows Desktop app

Development

# Clone and install
git clone https://github.com/brunobrown/flet-splash.git
cd flet-splash
uv sync

# Run tests
uv run pytest tests/ -v

# Lint and format
uv tool run ruff format
uv tool run ruff check
uv tool run ty check

# Run the CLI locally
uv run fs-build apk

Learn more

Flet Community

Join the community to contribute or get help:

Support

If you like this project, please give it a GitHub star


Contributing

Contributions and feedback are welcome!

  1. Fork the repository
  2. Create a feature branch
  3. Submit a pull request with detailed explanation

For feedback, open an issue with your suggestions.


Give your Flet app a professional first impression with flet-splash!

Commit your work to the LORD, and your plans will succeed. Proverbs 16:3

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

flet_splash-0.2.2.tar.gz (480.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

flet_splash-0.2.2-py3-none-any.whl (22.2 kB view details)

Uploaded Python 3

File details

Details for the file flet_splash-0.2.2.tar.gz.

File metadata

  • Download URL: flet_splash-0.2.2.tar.gz
  • Upload date:
  • Size: 480.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Linux Mint","version":"22.3","id":"zena","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for flet_splash-0.2.2.tar.gz
Algorithm Hash digest
SHA256 8543e8af1f2fcccb8b75c0f57db03c0991f7abf206e3097e7551773bfb29c5cd
MD5 70466f8c910cf324c91167d423f5a052
BLAKE2b-256 b62fd7b6536f7e40e58590e95335a02463fec67dba533065d6418fcdb00f3001

See more details on using hashes here.

File details

Details for the file flet_splash-0.2.2-py3-none-any.whl.

File metadata

  • Download URL: flet_splash-0.2.2-py3-none-any.whl
  • Upload date:
  • Size: 22.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Linux Mint","version":"22.3","id":"zena","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for flet_splash-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 88712842258af9190db820059061c163e94563eb2106ee7382dc69db29ccecf6
MD5 554b58e8de293b4009115ebce3208833
BLAKE2b-256 e80bc369e082464f2cce4936e6de9f79127e777edc0eda62e49af8457cbb631d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page