开源盈利模式
该项目采用:代码开源,GitHub Release 免费下载,应用商店付费下载的开源盈利方式。
Windows 商店 ¥29 元,Android 和 iOS 商店 1.99 刀。因为可以免费下载,商店售卖其实是一种赞助支持的方式。
商店版能够自动更新版本,也许免费下载版需要手动下载更新版本,这也是一点体验上的区别。
我觉得这种模式不错,值得借鉴。再加上一个捐赠渠道,就比较完整了。
技术栈选择
作者桌面端和移动端选用了不同的技术栈,是非常明智的。
当然,基于项目的背景,很可能是桌面端用前端技术栈先开发成功了,后面移动端选择一个新技术栈尝尝鲜,毕竟移动端上也没有 Electron 嘛。
回到技术上来,Flutter 在桌面端没有成熟的 WebView 支持,这成了限制 Flutter 在桌面平台发展的因素。
目前,Flutter 在 Windows 和 macOS 下只有一些第三方的简单封装,WebView2(Windows),WKWebView(macOS),还过于简陋。
因此,作者在不同平台下,都选择了 Web 能力强的框架,能够满足 RSS 阅读器的技术需求。
数据库
采用 SQLite 数据库。
数据表
数据表比我预想中要简单的多,2张表搞定:
<div about="#mwt13" class="mw-highlight mw-highlight-lang-dart mw-content-ltr" data-mw="{" name":"syntaxhighlight","attrs":{"lang":"dart"},"body":{"extsrc":"\nstatic="" future_onCreate(Database db, int version) async {\n await db.execute('''\n CREATE TABLE sources (\n sid TEXT PRIMARY KEY,\n url TEXT NOT NULL,\n iconUrl TEXT,\n name TEXT NOT NULL,\n openTarget INTEGER NOT NULL,\n latest INTEGER NOT NULL,\n lastTitle INTEGER NOT NULL\n );\n ''');\n await db.execute('''\n CREATE TABLE items (\n iid TEXT PRIMARY KEY,\n source TEXT NOT NULL,\n title TEXT NOT NULL,\n link TEXT NOT NULL,\n date INTEGER NOT NULL,\n content TEXT NOT NULL,\n snippet TEXT NOT NULL,\n hasRead INTEGER NOT NULL,\n starred INTEGER NOT NULL,\n creator TEXT,\n thumb TEXT\n );\n ''');\n await db.execute(\"CREATE INDEX itemsDate ON items (date DESC);\");\n}\n"}}" data-parsoid="{"dsr":[1162,1965,2,2]}" dir="ltr" typeof="mw:Extension/syntaxhighlight">static Future<void> _onCreate(Database db, int version) async {
await db.execute(''' CREATE TABLE sources ( sid TEXT PRIMARY KEY, url TEXT NOT NULL, iconUrl TEXT, name TEXT NOT NULL, openTarget INTEGER NOT NULL, latest INTEGER NOT NULL, lastTitle INTEGER NOT NULL ); ''');
await db.execute(''' CREATE TABLE items ( iid TEXT PRIMARY KEY, source TEXT NOT NULL, title TEXT NOT NULL, link TEXT NOT NULL, date INTEGER NOT NULL, content TEXT NOT NULL, snippet TEXT NOT NULL, hasRead INTEGER NOT NULL, starred INTEGER NOT NULL, creator TEXT, thumb TEXT ); ''');
await db.execute("CREATE INDEX itemsDate ON items (date DESC);");}
页面列表
一共十几个页面:
<div about="#mwt15" class="mw-highlight mw-highlight-lang-dart mw-content-ltr" data-mw="{" name":"syntaxhighlight","attrs":{"lang":"dart"},"body":{"extsrc":"\nstatic="" final="" mapbaseRoutes = {\n \"/article\": (context) => ArticlePage(),\n \"/error-log\": (context) => ErrorLogPage(),\n \"/settings\": (context) => SettingsPage(),\n \"/settings/sources\": (context) => SourcesPage(),\n \"/settings/sources/edit\": (context) => SourceEditPage(),\n \"/settings/feed\": (context) => FeedPage(),\n \"/settings/reading\": (context) => ReadingPage(),\n \"/settings/general\": (context) => GeneralPage(),\n \"/settings/about\": (context) => AboutPage(),\n \"/settings/service/fever\": (context) => FeverPage(),\n \"/settings/service/feedbin\": (context) => FeedbinPage(),\n \"/settings/service/inoreader\": (context) => InoreaderPage(),\n \"/settings/service/greader\": (context) => GReaderPage(),\n \"/settings/service\": (context) {\n"}}" data-parsoid="{"dsr":[1986,2811,2,2]}" dir="ltr" typeof="mw:Extension/syntaxhighlight">static final Map<String, Widget Function(BuildContext)> baseRoutes = {
"/article": (context) => ArticlePage(),
"/error-log": (context) => ErrorLogPage(),
"/settings": (context) => SettingsPage(),
"/settings/sources": (context) => SourcesPage(),
"/settings/sources/edit": (context) => SourceEditPage(),
"/settings/feed": (context) => FeedPage(),
"/settings/reading": (context) => ReadingPage(),
"/settings/general": (context) => GeneralPage(),
"/settings/about": (context) => AboutPage(),
"/settings/service/fever": (context) => FeverPage(),
"/settings/service/feedbin": (context) => FeedbinPage(),
"/settings/service/inoreader": (context) => InoreaderPage(),
"/settings/service/greader": (context) => GReaderPage(),
"/settings/service": (context) {
状态管理结构
采用 Provider 进行状态管理。
Global 全局单例
Global 中保存了所有的全局服务:
<div about="#mwt11" class="mw-highlight mw-highlight-lang-dart mw-content-ltr" data-mw="{" name":"syntaxhighlight","attrs":{"lang":"dart"},"body":{"extsrc":"\nabstract="" global="" {\n="" static="" bool="" _initialized="false;\n" globalmodel="" globalmodel;\n="" sourcesmodel="" sourcesmodel;\n="" itemsmodel="" itemsmodel;\n="" feedsmodel="" feedsmodel;\n="" groupsmodel="" groupsmodel;\n="" syncmodel="" syncmodel;\n="" servicehandler="" service;\n="" database="" db;\n="" jaguar="" server;\n="" final="" globalkeytabletPanel = GlobalKey();\n"}}" data-parsoid="{"dsr":[2886,3339,2,2]}" dir="ltr" typeof="mw:Extension/syntaxhighlight">abstract class Global {
static bool _initialized = false;
static GlobalModel globalModel;
static SourcesModel sourcesModel;
static ItemsModel itemsModel;
static FeedsModel feedsModel;
static GroupsModel groupsModel;
static SyncModel syncModel;
static ServiceHandler service;
static Database db;
static Jaguar server;
static final GlobalKey<NavigatorState> tabletPanel = GlobalKey();
其中:
Global 本身是个抽象类:比较巧妙,不是为了继承,是为了防止创建单例
里面全是静态成员和静态方法
有一个 init 方法用于初始化
项目中:
Provider 状态提供
@overrideWidget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: Global.globalModel),
ChangeNotifierProvider.value(value: Global.sourcesModel),
ChangeNotifierProvider.value(value: Global.itemsModel),
ChangeNotifierProvider.value(value: Global.feedsModel),
ChangeNotifierProvider.value(value: Global.groupsModel),
ChangeNotifierProvider.value(value: Global.syncModel),
],
Model 与 SP 打通
配置信息由 SharedPreference 提供,并通过 Provider 直接响应式通知出去:
_theme;\n set theme(ThemeSetting value) {\n if (value != _theme) {\n _theme = value;\n notifyListeners();\n Store.setTheme(value);\n }\n }\n"}}" data-parsoid="{"dsr":[4147,4794,2,2]}" dir="ltr" typeof="mw:Extension/syntaxhighlight">
class GlobalModel with ChangeNotifier {
ThemeSetting _theme = Store.getTheme();
Locale _locale = Store.getLocale();
int _keepItemsDays = Store.sp.getInt(StoreKeys.KEEP_ITEMS_DAYS) ?? 21;
bool _syncOnStart = Store.sp.getBool(StoreKeys.SYNC_ON_START) ?? true;
bool _inAppBrowser = Store.sp.getBool(StoreKeys.IN_APP_BROWSER) ?? Platform.isIOS;
double _textScale = Store.sp.getDouble(StoreKeys.TEXT_SCALE);
ThemeSetting get theme => _theme;
set theme(ThemeSetting value) {
if (value != _theme) {
_theme = value;
notifyListeners();
Store.setTheme(value);
}
}
同步服务
还支持与外部服务同步,包括 Fever、Feedbin、GReader、Inoreader。
采用代理模式,接口抽象地很清晰。
原文:https://maxieewong.com/Fluent-reader-lite%20%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html?continueFlag=ead1c62ec98fc6af93e8cdb64f2790db