flutter能够构建跨平台的多端应用, 正好开发的应用须要桌面版本, 那就尝试传说中的无缝移植.linux
然而刚开始就遇到了大麻烦: 移动端广泛使用的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)
的异常.android
这"无缝"来的太猛, 有点措手不及...等着官方出正式版本断然不行的, 必须得自行添加在平台层的实现了. 好在桌面端能够以插件的方式与shared_preferences
对接上, 结合在macOS上的实现及提供的示例程序总算给搞出来了! 以linux为例, 写一下久违的C++.ios
开发环境: 以前尝试最新的flutter1.9运行集成的桌面应用,但失败了, 因此开发环境在flutter1.8, 这是肯定能够运行起来的c++
flutterSDK: v1.8.0@stablegit
flutter Desktop: c183d46798b9642b8d908710de1e7d14a8573c86@mastergithub
pubspec.yaml:web
dependencies: shared_preferences: ^0.5.3+4
运行如下命令确保能够运行起来或者参照这篇文章 (flutterSDK安装再也不另行说明):shell
git clone https://github.com/google/flutter-desktop-embedding.git desktop cd desktop/example flutter run
咱们就是基于example应用把SharedPreferences插件开发出来.macos
插件结构
全部的插件位于desktop仓库根目录下的plugins
, 其中的flutter_plugins
特指的是flutter在其它端(android/iOS/web)也能够用的插件, 其他的表示只在桌面端(macOS/linux/windows)用到的插件, 须要实现的SharedPreferences
就在plugins/flutter_plugins/shared_preferences_fde
下,能够看到只有macos的目录. 因此开始新建linux平台上的插件:json
- 建立目录及文件
借助已经有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了.