No project description provided
Project description
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.
Table of Contents
- The Problem
- The Solution
- Installation
- Quick Start
- How It Works
- Build Process in Detail
- Configuration
- Splash Types
- Text Overlay
- Dark Mode Support
- Understanding
min_duration - CLI Reference
- Important Notes
- Examples
- Supported Platforms
- Development
- Buy Me a Coffee
- Learn more
- Flet Community
- Support
- Contributing
Buy Me a Coffee
If you find this project useful, please consider supporting its development:
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, andpubspec.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, andcustomrequire asourcefile - Type
customrequires a.dartextension - 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 everything — BlankScreen, 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:
- App starts →
_SplashBootstraprenders aStackwith the app behind andCustomSplashon top (opacity 1.0) - The app boots normally underneath (invisible to the user)
min_durationtimer runs in parallel with app initialization- When
prepareApp()completes →_appReady.value = true - The splash fades only when both: timer elapsed AND app is ready
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
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
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.
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.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.
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", thebackgroundanddark_backgroundsettings 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:
- The
min_durationtimer has elapsed - The app has finished initializing (
prepareApp()completed)
This means:
- If
min_durationis longer than the initialization time → splash stays for the fullmin_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 betweenprepareApp()completing and the app content appearing on screen. Settingmin_duration = 0would cause the splash to fade during this gap, revealing a white screen. A highermin_durationensures 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!
- Fork the repository
- Create a feature branch
- 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8543e8af1f2fcccb8b75c0f57db03c0991f7abf206e3097e7551773bfb29c5cd
|
|
| MD5 |
70466f8c910cf324c91167d423f5a052
|
|
| BLAKE2b-256 |
b62fd7b6536f7e40e58590e95335a02463fec67dba533065d6418fcdb00f3001
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
88712842258af9190db820059061c163e94563eb2106ee7382dc69db29ccecf6
|
|
| MD5 |
554b58e8de293b4009115ebce3208833
|
|
| BLAKE2b-256 |
e80bc369e082464f2cce4936e6de9f79127e777edc0eda62e49af8457cbb631d
|