为什么我停止开发应用,转而在 Flutter 中构建引擎
I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please paste the content (excluding the source link you already provided) here? Once I have the article’s body, I’ll translate it into Simplified Chinese while preserving the original formatting, markdown, and any code blocks.
介绍
作为独立开发者,最大的瓶颈不是技能,而是时间。维护多个应用程序会迅速摧毁你的生产力。
问题
几个月前,我开始构建 PyMaster,这是一款在手机上帮助人们学习 Python 的游戏化应用。发布第一个版本后,我立刻想加入 SQL、JavaScript,甚至 Rust。显而易见的做法是复制代码库、更换图标、修改内容,然后发布新应用。
我尝试了一下:克隆了仓库,重命名了一些东西,十分钟内我已经对看到的东西感到厌烦。
- 三个独立的代码库
- 三套独立的 bug
- 三次需要为连击逻辑推送修复
解决方案:白标引擎
与其再构建一个新应用,我选择构建一个引擎——一个代码库可以编译成无限数量的应用。一个由 Flutter + Riverpod 驱动的白标教育平台。
将 UI 与内容解耦
在普通应用中,UI 知道太多:
Text("Welcome to Python")它还包含只能理解特定语言的紧耦合解析器。要实现白标化,UI 必须变得完全“傻瓜”,只能渲染传入的内容。
核心契约:CourseBlueprint
// 每个应用风味必须实现的主契约
abstract class CourseBlueprint {
String get appTitle;
String get virtualCurrencyName;
// 主题引擎
Color get brandPrimaryColor;
Color get brandSecondaryColor;
// 逻辑注入
CodeParserStrategy get codeParser;
// 每个应用的第三方配置
String get analyticsId;
String get aiSystemPrompt;
}每种风味(Python、SQL、JavaScript …)都实现此接口。UI 永不直接导入具体的解析器,只是从配置中获取解析器。
示例:Python 风味
// 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.";
}要推出新风味(例如 SQL),只需创建 SqlCourseConfig,更改主题颜色,注入 SqlKeywordParser,并更新 AI 提示即可。
选择正确的配置
在编译时,我使用 Flutter 的 --dart-define 标志。一个小工厂读取风味并返回相应的配置:
class BlueprintFactory {
static CourseBlueprint getForFlavor(String flavor) {
switch (flavor.toLowerCase()) {
case 'python':
return PythonCourseConfig();
case 'sql':
return SqlCourseConfig();
default:
return PythonCourseConfig(); // Safe fallback
}
}
}常见的坑:忘记传递构建标志(--dart-define=APP_FLAVOR=sql)会导致应用回退到默认的 Python 主题。
使用 Riverpod 实现全局访问
Riverpod 消除了属性传递的需求:
// 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(),
),
);
}任何 widget 现在都可以读取配置:
Widget build(BuildContext context, WidgetRef ref) {
final config = ref.watch(blueprintProvider);
return Text(
"Welcome to ${config.appTitle}!",
style: TextStyle(color: config.brandPrimaryColor),
);
}UI 完全对业务领域保持盲目,只负责渲染它收到的内容。
好处
- 单一事实来源: 只需在核心游戏化引擎(连胜、XP、离线进度)中修复一次错误,修复即可传播到所有版本。
- 快速迭代: 启动一个新应用只需数小时,而不是数周。
- 专注投入: 我的时间大部分现在用于内容,而不是基础设施。
结果
我发布了第一个版本 PyMaster,现已在 Google Play 商店上线。它的特性包括:
- 游戏化的离线进度
- 动态主题引擎
- 内置 AI 导师
讨论
您是否设计过类似的架构?您选择了:
- 应用变体
- 单体仓库
- 插件架构
我很好奇您遇到的权衡取舍。请在下方评论留下您的想法。