flutter: SharedPreferences桌面插件

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;
複製代碼

這樣就能夠運行flutter run了, 結果雖然仍是有異常, 但錯誤信息應該是咱們寫死的'"no result", "but great~!" '代表方法成功調用了~

注意 這期間編譯的時候最好是先把example/build目錄刪除, 這樣生成的是最新的中間文件, 不然因爲緩存舊的生成文件可能致使一些運行時異常退出的詭異問題.

插件實現

最後一步天然是咱們要如何在平臺層實現SharedPreferences的key/value存儲功能. 由於已經有了shared_preferencesdart包, 因此實現其對應的接口就好.

名稱 用途
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了.

相關文章
相關標籤/搜索