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.
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.
- The Problem
- The Solution
- Installation
- Quick Start
- How It Works
- Build Process in Detail
- Configuration
- Splash Types
- Text Overlay
- Dark Mode Support
- CLI Reference
- Important Notes
- Examples
- Supported Platforms
- Development
- Buy Me a Coffee
- Learn more
- Flet Community
- Support
- Contributing
If you find this project useful, please consider supporting its development:
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.
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.
# 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.gitRequirements: Python 3.10+, Flet 0.80.0+
1. Configure your splash in pyproject.toml:
[tool.flet.splash]
type = "color"
background = "#1a1a2e"
min_duration = 2.02. Build your app:
fs-build apkThat's it. The splash screen is automatically injected into the Flutter build.
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, andpubspec.yaml - Smart — if the Flutter project already exists, skips the first pass
| 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) |
This section explains exactly what happens when you run fs-build apk — from reading your config to delivering the final APK.
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, andcustomrequire asourcefile - Type
customrequires a.dartextension - The source file must exist on disk
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)
The injection modifies build/flutter/lib/main.dart in 5 sequential patches:
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'; // ← addedTypes color, image, and custom don't add any imports.
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.
All BlankScreen() constructor calls in the code are replaced with CustomSplash():
// BEFORE
return const MaterialApp(home: BlankScreen());
// AFTER
return const MaterialApp(home: CustomSplash());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 everything — BlankScreen, LoadingPage, and the app itself.
The _SplashBootstrap widget is appended at the end of main.dart. This is the core mechanism that creates the overlay effect:
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;
@override
void initState() {
super.initState();
// After min_duration, start hiding the splash
Future.delayed(const Duration(milliseconds: 2000), () {
if (mounted) setState(() => _showSplash = false);
});
}
@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:
- App starts →
_SplashBootstraprenders aStackwith the app behind andCustomSplashon top (opacity 1.0) - The app boots normally underneath (invisible to the user)
- After
min_durationms →_showSplashbecomesfalse AnimatedOpacityfades from 1.0 to 0.0 overfade_durationmsIgnorePointerallows touch events to pass through during the fade- The splash becomes fully transparent → the app is revealed
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-splashThe asset is appended at the end of the existing assets: list, preserving the original indentation.
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).
Finally, flet build runs again. This time the Flutter project already contains:
- The
CustomSplashwidget (replacingBlankScreen) - The
_SplashBootstrapoverlay wrappingrunApp - 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.
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
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 = 2.0 # minimum splash duration in seconds
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 pixelsAny 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 16Priority: 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-abiA solid color background. Simplest option — no external assets needed.
[tool.flet.splash]
type = "color"
background = "#1a1a2e"
dark_background = "#0d0d1a"A static image (PNG, JPG, GIF, WebP) centered on the splash screen.
[tool.flet.splash]
type = "image"
source = "assets/custom_splash.png"
background = "#0d47a1"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.0Tip: Download free Lottie animations from LottieFiles.
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.svginside theassets/folder. Flet automatically detectsassets/splash.*files and passes them toflutter_native_splash, which does not support SVG format. Use a different name likecustom_splash.svg.
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.0The .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", thebackgroundanddark_backgroundsettings are ignored — your widget controls everything.
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 = 16The text is rendered as a Flutter Text widget positioned below the splash body.
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.
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 buildFlet 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.
flet-splash modifies files inside build/flutter/. If you encounter issues, use --clean to start fresh:
fs-build apk --cleanThe 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.
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 apkflet-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 |
# 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 apkJoin the community to contribute or get help:
If you like this project, please give it a GitHub star ⭐
Contributions and feedback are welcome!
- Fork the repository
- Create a feature branch
- Submit a pull request with detailed explanation
For feedback, open an issue with your suggestions.
Commit your work to the LORD, and your plans will succeed. Proverbs 16:3
