Why I Stopped Building Apps and Started Building an Engine in Flutter
Source: Dev.to
Introduction
As an indie developer, the biggest bottleneck isn’t skill—it’s time. Maintaining multiple apps can quickly destroy your productivity.
The Problem
A few months ago I started building PyMaster, a gamified app to help people learn Python on their phones. After shipping the first version, I immediately wanted to add SQL, JavaScript, maybe even Rust. The obvious move was to duplicate the codebase, swap logos, change content, and publish a new app.
I tried it: cloned the repo, renamed a few things, and within ten minutes I already hated what I was looking at.
- Three separate codebases
- Three separate sets of bugs
- Three separate times I’d have to push a fix for the streak logic
As a solo founder, that’s not a roadmap—it’s a slow death.
The Solution: A White‑Label Engine
Instead of building another app, I built an engine—one codebase that can compile into an infinite number of apps. A white‑label educational platform powered by Flutter + Riverpod.
Decoupling UI from Content
In a normal app the UI knows too much:
Text("Welcome to Python")It also contains tightly coupled parsers that only understand a specific language. To white‑label this, the UI must become completely dumb—it should only render what it’s given.
Core Contract: CourseBlueprint
// The master contract every app flavor must fulfill
abstract class CourseBlueprint {
String get appTitle;
String get virtualCurrencyName;
// Theme Engine
Color get brandPrimaryColor;
Color get brandSecondaryColor;
// Logic Injection
CodeParserStrategy get codeParser;
// Per‑app 3rd Party Config
String get analyticsId;
String get aiSystemPrompt;
}Every flavor (Python, SQL, JavaScript, …) implements this interface. The UI never imports a concrete parser; it simply asks for a parser supplied by the configuration.
Example: Python Flavor
// Python‑specific implementation
class PythonCourseConfig implements CourseBlueprint {
@override
String get appTitle => "PyMaster";
@override
String get virtualCurrencyName => "Tokens";
@override
Color get brandPrimaryColor => const Color(0xFFFFD43B); // Python Yellow
@override
Color get brandSecondaryColor => const Color(0xFF306998); // Python Blue
@override
CodeParserStrategy get codeParser => PythonCodeParser(); // Handles indentation
@override
String get analyticsId => "UA-XXXXX-PYTHON";
@override
String get aiSystemPrompt =>
"You are an expert Python tutor. Explain the error in simple terms.";
}Launching a new flavor (e.g., SQL) is as simple as creating SqlCourseConfig, changing the theme colors, injecting a SqlKeywordParser, and updating the AI prompt.
Selecting the Right Configuration
During compilation I use Flutter’s --dart-define flag. A small factory reads the flavor and returns the appropriate config:
class BlueprintFactory {
static CourseBlueprint getForFlavor(String flavor) {
switch (flavor.toLowerCase()) {
case 'python':
return PythonCourseConfig();
case 'sql':
return SqlCourseConfig();
default:
return PythonCourseConfig(); // Safe fallback
}
}
}A common pitfall: forgetting to pass the build flag (--dart-define=APP_FLAVOR=sql) caused the app to fall back to the default Python theme.
Global Access with Riverpod
Riverpod eliminates the need for prop‑drilling:
// Global provider — overridden at startup
final blueprintProvider = Provider((ref) {
throw UnimplementedError("Must be overridden in main.dart");
});
void main() {
const flavor = String.fromEnvironment('APP_FLAVOR', defaultValue: 'python');
final selectedConfig = BlueprintFactory.getForFlavor(flavor);
runApp(
ProviderScope(
overrides: [
blueprintProvider.overrideWithValue(selectedConfig),
],
child: const MyEduApp(),
),
);
}Any widget can now read the configuration:
Widget build(BuildContext context, WidgetRef ref) {
final config = ref.watch(blueprintProvider);
return Text(
"Welcome to ${config.appTitle}!",
style: TextStyle(color: config.brandPrimaryColor),
);
}The UI is completely blind to the domain; it simply renders what it receives.
Benefits
- Single source of truth: Fix a bug in the core gamification engine (streaks, XP, offline progression) once, and the fix propagates to every flavor.
- Rapid iteration: Spinning up a new app takes hours instead of weeks.
- Focused effort: Most of my time is now spent on content rather than infrastructure.
Outcome
I shipped the first flavor, PyMaster, which is now live on the Google Play Store. It features:
- Gamified offline progression
- A dynamic theme engine
- A built‑in AI tutor
Discussion
Have you architected something similar? Did you choose:
- App flavors
- Monorepos
- Plugin architectures
I’m curious about the trade‑offs you encountered. Drop your thoughts in the comments below.