共计 9968 个字符,预计需要花费 25 分钟才能阅读完成。
flutter 可以构建跨平台的多端应用, 正好开发的应用需要桌面版本, 那就尝试传说中的无缝移植.
然而刚开始就遇到了大麻烦: 移动端普遍使用的 SharedPreferences 在桌面端只有 macOS 有实现! 虽然引入 shared_preferences: ^0.5.3+4
在编译时没有问题, 但 windows 和 linux 平台在运行时会抛出 [ERROR:flutter/lib/ui/ui_dart_state.cc(148)] Unhandled Exception: MissingPluginException(No implementation found for method getAll on channel plugins.flutter.io/shared_preferences)
的异常.
这 ” 无缝 ” 来的太猛, 有点措手不及 … 等着官方出正式版本断然不行的, 必须得自行添加在平台层的实现了. 好在桌面端可以以插件的方式与 shared_preferences
对接上, 结合在 macOS 上的实现及提供的示例程序总算给搞出来了! 以 linux 为例, 写一下久违的 C ++.
开发环境:
之前尝试最新的 flutter1.9 运行集成的桌面应用, 但失败了, 所以开发环境在 flutter1.8, 这是确定可以运行起来的
flutterSDK: v1.8.0@stable
flutter Desktop: c183d46798b9642b8d908710de1e7d14a8573c86@master
pubspec.yaml:
dependencies:
shared_preferences: ^0.5.3+4
运行以下命令确保可以运行起来或者参照这篇文章 (flutterSDK 安装不再另行说明):
git clone https://github.com/google/flutter-desktop-embedding.git desktop
cd desktop/example
flutter run
我们就是基于 example 应用把 SharedPreferences 插件开发出来.
插件结构
所有的插件位于 desktop 仓库根目录下的 plugins
, 其中的flutter_plugins
特指的是 flutter 在其它端 (android/iOS/web) 也可以用的插件, 其余的表示只在桌面端 (macOS/linux/windows) 用到的插件, 需要实现的 SharedPreferences
就在 plugins/flutter_plugins/shared_preferences_fde
下, 可以看到只有 macos 的目录.
所以开始新建 linux 平台上的插件:
- 创建目录及文件
借助已经有url_launcher_fde
mkdir -p plugins/flutter_plugins/shared_preferences_fde/linux && cd plugins/flutter_plugins/shared_preferences_fde/linux
cp ../../url_launcher_fde/linux/Makefile .
cp ../../url_launcher_fde/linux/url_launcher_fde_plugin.{cc,h} .
- 插件命名
将 Makefile 中的 url_launcher_fde_plugin
改成 shared_preferences_fde_plugin
, 这是编译插件所需要的 Makefile, 只需改这一个名称即可.
本地 cpp 文件改成 shared_preferences_fde_plugin.{cc,h}
, 同时类名和宏也改成相应的名称, 最好用sed
搜索一起替换
FLUTTER_PLUGIN_EXPORT void SharedPreferencesRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar);
class SharedPreferencesPlugin : public flutter::Plugin {virtual ~SharedPreferencesPlugin();
private:
SharedPreferencesPlugin();}
...
RegisterWithRegistrar
方法里有个通道注册的名称"plugins.flutter.io/shared_preferences"
, 这和异常抛出时的名称是一致的.
void SharedPreferencesPlugin::RegisterWithRegistrar(flutter::PluginRegistrar *registrar) {
auto channel = std::make_unique<flutter::MethodChannel<EncodableValue>>(registrar->messenger(), "plugins.flutter.io/shared_preferences",
&flutter::StandardMethodCodec::GetInstance());
}
另外需要专门说一下 SharedPreferencesPlugin::HandleMethodCall
这个方法
void SharedPreferencesPlugin::HandleMethodCall(
const flutter::MethodCall<EncodableValue> &method_call,
std::unique_ptr<flutter::MethodResult<EncodableValue>> result) {}
method_call
是方法调用结构体, 包含 dart 层传过来的名称参数等信息, 以引用的类型传入; result
是方法结果结构体, 包含需要传回给 dart 层的返回值及操作结果标识(标识这个调用是否成功), 以指针类型传入.
dart 和 c ++ 两种语言的数据类型完全不一样, 是怎么相互传递的? 这就用到了一个很重要的数据结构 flutter::EncodableValue
, EncodableValue
在 c ++ 层抽象了 dart 层的数据类型, 一个实例可以作为 bool, int, String, map, 与 list:
EncodableValue b(true); // 作为 bool 的 EncodableValue
EncodableValue v(32); // 作为 int 的 EncodableValue
EncodableValue ret(EncodableValue::Type::kMap); // 作为 map 的 EncodableValue
EncodableMap& map = ret.MapValue(); // 操作起来必须先转成 EncodableMap 类型
std::string key = "some_key";
map[EncodableValue(key)] = v; // EncodableMap 的 K / V 也必须是 EncodableValue
flutter 引擎最后完成到 dart 类型的最终对应.
插件依赖 shared_preference 的 dart 包, 所以需要看 $FLUTTER_SDK/.pub-cache/hosted/$PUB_HOST/shared_preferences-0.5.3+4/lib/shared_preferences.dart
传递和需要哪些数据.
初始化用到的方法名是 ’getAll’, 需要返回已经存储的所有键值对, 可以先实现一个空方法来通过编译环节:
void SharedPreferencesPlugin::HandleMethodCall(
const flutter::MethodCall<EncodableValue> &method_call,
std::unique_ptr<flutter::MethodResult<EncodableValue>> result) {const auto methodName = method_call.method_name();
if (methodName.compare("getAll") == 0) {result->Error("no result", "but great~!");
} else {result->NotImplemented();
}
}
关联插件
生成插件
在构建应用的时候把插件也编译进来, 所以需要改造 Makefile(注意是应用的 Makefile, 不是插件的), 如果是 windows 得改 sln 文件, 总之就是得关联上, 改造后的 example/linux/Makefile
如下:
# Executable name.
BINARY_NAME=flutter_desktop_example
# The C++ code for the embedder application.
SOURCES=flutter_embedder_example.cc
FLUTTER_PLUGIN_NAMES=shared_preferences_fde
# Default build type. For a release build, set BUILD=release.
# Currently this only sets NDEBUG, which is used to control the flags passed
# to the Flutter engine in the example shell, and not the complation settings
# (e.g., optimization level) of the C++ code.
BUILD=debug
# Configuration provided via flutter tool.
include flutter/generated_config
# Dependency locations
FLUTTER_APP_CACHE_DIR=flutter
FLUTTER_APP_DIR=$(CURDIR)/..
FLUTTER_APP_BUILD_DIR=$(FLUTTER_APP_DIR)/build
PLUGINS_DIR=$(CURDIR)/../../plugins
FLUTTER_PLUGINS_DIR=$(PLUGINS_DIR)/flutter_plugins
OUT_DIR=$(FLUTTER_APP_BUILD_DIR)/linux
# Libraries
FLUTTER_LIB_NAME=flutter_linux
FLUTTER_LIB=$(FLUTTER_APP_CACHE_DIR)/lib$(FLUTTER_LIB_NAME).so
PLUGIN_LIB_NAMES=$(foreach plugin,$(PLUGIN_NAMES) $(FLUTTER_PLUGIN_NAMES),$(plugin)_plugin)
PLUGIN_LIBS=$(foreach plugin,$(PLUGIN_LIB_NAMES),$(OUT_DIR)/lib$(plugin).so)
ALL_LIBS=$(FLUTTER_LIB) $(PLUGIN_LIBS)
# Tools
FLUTTER_BIN=$(FLUTTER_ROOT)/bin/flutter
LINUX_BUILD=$(FLUTTER_ROOT)/packages/flutter_tools/bin/linux_backend.sh
# Resources
ICU_DATA_NAME=icudtl.dat
ICU_DATA_SOURCE=$(FLUTTER_APP_CACHE_DIR)/$(ICU_DATA_NAME)
FLUTTER_ASSETS_NAME=flutter_assets
FLUTTER_ASSETS_SOURCE=$(FLUTTER_APP_BUILD_DIR)/$(FLUTTER_ASSETS_NAME)
# Bundle structure
BUNDLE_OUT_DIR=$(OUT_DIR)/$(BUILD)
BUNDLE_DATA_DIR=$(BUNDLE_OUT_DIR)/data
BUNDLE_LIB_DIR=$(BUNDLE_OUT_DIR)/lib
BIN_OUT=$(BUNDLE_OUT_DIR)/$(BINARY_NAME)
ICU_DATA_OUT=$(BUNDLE_DATA_DIR)/$(ICU_DATA_NAME)
FLUTTER_LIB_OUT=$(BUNDLE_LIB_DIR)/$(notdir $(FLUTTER_LIB))
ALL_LIBS_OUT=$(foreach lib,$(ALL_LIBS),$(BUNDLE_LIB_DIR)/$(notdir $(lib)))
# Add relevant code from the wrapper library, which is intended to be statically
# built into the client.
WRAPPER_ROOT=$(FLUTTER_APP_CACHE_DIR)/cpp_client_wrapper
WRAPPER_SOURCES= \
$(WRAPPER_ROOT)/flutter_window_controller.cc \
$(WRAPPER_ROOT)/plugin_registrar.cc \
$(WRAPPER_ROOT)/engine_method_result.cc
SOURCES+=$(WRAPPER_SOURCES)
# Headers
WRAPPER_INCLUDE_DIR=$(WRAPPER_ROOT)/include
PLUGIN_INCLUDE_DIRS=$(OUT_DIR)/include
INCLUDE_DIRS=$(FLUTTER_APP_CACHE_DIR) $(WRAPPER_INCLUDE_DIR) $(PLUGIN_INCLUDE_DIRS)
# Build settings
CXX=clang++
CXXFLAGS.release=-DNDEBUG
CXXFLAGS=-std=c++14 -Wall -Werror $(CXXFLAGS.$(BUILD))
CPPFLAGS=$(patsubst %,-I%,$(INCLUDE_DIRS))
LDFLAGS=-L$(BUNDLE_LIB_DIR) \
-l$(FLUTTER_LIB_NAME) \
$(patsubst %,-l%,$(PLUGIN_LIB_NAMES)) \
-ljsoncpp \
-Wl,-rpath=\$$ORIGIN/lib
# Targets
.PHONY: all
all: $(BIN_OUT) bundle
# This is a phony target because the flutter tool cannot describe
# its inputs and outputs yet.
.PHONY: sync
sync: flutter/generated_config
$(FLUTTER_ROOT)/packages/flutter_tools/bin/tool_backend.sh linux-x64 $(BUILD)
.PHONY: bundle
bundle: $(ICU_DATA_OUT) $(ALL_LIBS_OUT) bundleflutterassets
$(BIN_OUT): $(SOURCES) $(ALL_LIBS_OUT)
mkdir -p $(@D)
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(SOURCES) $(LDFLAGS) -o $@
$(WRAPPER_SOURCES) $(FLUTTER_LIB) $(ICU_DATA_SOURCE) $(FLUTTER_ASSETS_SOURCE): \
| sync
$(OUT_DIR)/libshared_preferences_fde_plugin.so: | shared_preferences_fde
.PHONY: $(FLUTTER_PLUGIN_NAMES)
$(FLUTTER_PLUGIN_NAMES):
make -C $(FLUTTER_PLUGINS_DIR)/$@/linux \
OUT_DIR=$(OUT_DIR) FLUTTER_ROOT=$(FLUTTER_ROOT)
# Plugin library bundling pattern.
$(BUNDLE_LIB_DIR)/%: $(OUT_DIR)/%
mkdir -p $(BUNDLE_LIB_DIR)
cp $< $@
$(FLUTTER_LIB_OUT): $(FLUTTER_LIB)
mkdir -p $(BUNDLE_LIB_DIR)
cp $(FLUTTER_LIB) $(BUNDLE_LIB_DIR)
$(ICU_DATA_OUT): $(ICU_DATA_SOURCE)
mkdir -p $(dir $(ICU_DATA_OUT))
cp $(ICU_DATA_SOURCE) $(ICU_DATA_OUT)
# Fully re-copy the assets directory on each build to avoid having to keep a
# comprehensive list of all asset files here, which would be fragile to changes
# in the Flutter example (e.g., adding a new font to pubspec.yaml would require
# changes here).
.PHONY: bundleflutterassets
bundleflutterassets: $(FLUTTER_ASSETS_SOURCE)
mkdir -p $(BUNDLE_DATA_DIR)
rsync -rpu --delete $(FLUTTER_ASSETS_SOURCE) $(BUNDLE_DATA_DIR)
.PHONY: clean
clean:
rm -rf $(OUT_DIR); \
cd $(FLUTTER_APP_DIR); \
$(FLUTTER_BIN) clean
diff 一下容易看出来, 本质是构建应用的时候增加一个依赖 (ALL_LIBS_OUT
), 这个依赖是一些.so 文件, 这些.so 文件根据我们给定的插件目录(FLUTTER_PLUGINS_DIR
) 下的插件名称 (FLUTTER_PLUGIN_NAMES
) 在指定目录 (OUT_DIR
) 生成.
加载插件
生成完成后需要加载, 这个加载是静态的, 也就是编译时显式的通过代码调用, 直接上 example/linux/flutter_embedder_example.cc
的 diff 文件
index d87734f..bbc203d 100644
@@ -21,6 +21,8 @@
#include <flutter/flutter_window_controller.h>
+#include <shared_preferences_fde_plugin.h>
+
namespace {
// Returns the path of the directory containing this executable, or an empty
@@ -65,6 +67,9 @@ int main(int argc, char **argv) {return EXIT_FAILURE;}
+ SharedPreferencesRegisterWithRegistrar(+ flutter_controller.GetRegistrarForPlugin("SharedPreferences"));
+
// Run until the window is closed.
flutter_controller.RunEventLoop();
return EXIT_SUCCESS;
这样就可以运行了, 结果虽然还是有异常, 但错误信息应该是我们写死的 '”no result”, “but great~!” ‘ 表明方法成功调用了~
注意 这期间编译的时候最好是先把 example/build 目录删除, 这样生成的是最新的中间文件, 否则由于可能缓存旧的生成文件导致一些运行时异常退出的诡异问题
插件实现
最后一步自然是我们要如何在平台层实现 SharedPreferences
的 key/value 存储功能. 因为已经有了shared_preferences
dart 包, 所以实现其对应的接口就好.
名称 | 用途 |
---|---|
getAll | 初始化时返回所有 k /v |
commit | 将改动保存 |
clear | 清除所有 k /v |
remove | 移除某项 |
setBool | 存 bool |
setInt | 存 int |
搜索了一圈发现 linux 竟然没有广泛使用的 K / V 存储库! 大概桌面的应用长久以来数据一般都直接存文件.
后来看到 flutter-go
)项目实现 SharedPreferences
用的是 levelDB, 于是也就欣然用之, 结果发现很不好用! levelDB 的 Key/Value 可以是任意长度的字节数组, 强大是强大, 可用在这里却不合适, 因为取数据的时候丢失了类型信息, 无法知道 key 对应的这个 value 到底是 int 还是 bool, 除非在存数据时候再设计出类型存储的格式. 这太麻烦了.
想到 shared_preference 在 android 底层也是个 xml 文件, 而且需要知道类型, 同时当前也不用太考虑性能问题, 那直接以 json 存不就完了吗?! 于是很快找到 jsoncpp 这个库, 容易上手, 直接操作文件, 而且读取之后能够知道数据的类型信息, 完美!
‘getAll’ 方法如下:
if (methodName.compare("getAll") == 0) {
std::ifstream infile;
infile.open(kSavedFileName, std::ios::in);
try {infile >> _root;} catch (std::exception& e) {_root = Json::objectValue;}
infile.close();
EncodableValue ret(EncodableValue::Type::kMap);
EncodableMap& map = ret.MapValue();
for (auto i = _root.begin(); i != _root.end(); i++) {
Json::Value& obj = *i;
const std::string key = i.name();
map[EncodableValue(key)] = adaptJsonValue(obj);
}
result->Success(&ret);
} else if (methodName.find("remove") == 0) {
adaptJsonValue
方法只是把 jsoncpp 的类型转成 flutter 对应的类型
更多 Json::Value
用法见之前写的指南
static EncodableValue adaptJsonValue(const Json::Value& value) {switch (value.type()) {
case Json::nullValue: {return EncodableValue(EncodableValue::Type::kNull);
}
case Json::booleanValue: {bool v = value.asBool();
return EncodableValue(v);
}
case Json::uintValue:
case Json::intValue: {int v = value.asInt();
return EncodableValue(v);
}
case Json::realValue: {double v = value.asDouble();
return EncodableValue(v);
}
case Json::arrayValue: {EncodableValue ev(EncodableValue::Type::kList);
flutter::EncodableList& v = ev.ListValue();
Json::Value def;
for (Json::ArrayIndex i = 0; i < value.size(); ++i) {v.push_back(adaptJsonValue(value.get(i, def)));
}
return ev;
}
case Json::objectValue: {return EncodableValue();
}
case Json::stringValue:
default: {const char* v = value.asCString();
return EncodableValue(v);
}
}
}
最终在 flutter 项目中 dart 层的代码再验证一下就 OK 了.