Flitter: SharedPreferences desktop plug-in

Time:2020-3-21

Flutter can build multi-terminal applications across platforms. If the developed applications need desktop version, try the legendary seamless migration

However, at the very beginning, there was a big problem: shared preferences, which is widely used in mobile terminals, is only implemented in Mac OS on the desktop! Although the introduction ofshared_preferences: ^0.5.3+4No problem at compile time, but windows and Linux platforms throw[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)Exception.

This “seamless” is too fierce, a little unprepared… Waiting for the official version to come out, it’s absolutely not good. You have to add it to the platform layer by yourself. Fortunately, the desktop side can connect with theshared_preferencesOn the docking, the example program combined with the implementation and provided on Mac OS is finally worked out! Take Linux as an example, write the long lost C + +

Development environment:
Previous attempts to run the integrated desktop application with the latest fluent 1.9 failed, so the development environment is in fluent 1.8, which is sure to run

flutterSDK: [email protected]

flutter Desktop: [email protected]

pubspec.yaml:

dependencies:
  shared_preferences: ^0.5.3+4

Run the following command to make sure it can be run or refer to this article (the installation of the fluttersdk will not be described separately):

git clone https://github.com/google/flutter-desktop-embedding.git desktop
cd desktop/example
flutter run

We developed the shared preferences plug-in based on the example application

Plug-in structure

All plug-ins are located in thepluginsAmong themflutter_pluginsIn particular, it refers to plugins that can be used in other terminals (Android / IOS / WEB), and other plugins that are only used in desktop (MAC OS / Linux / windows) need to be implementedSharedPreferencesJust inplugins/flutter_plugins/shared_preferences_fdeYou can see the directory with only MacOS
So start to create new plug-ins on Linux platform:

  1. Create directory and file

With the help of existingurl_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} .
  1. Plug-in naming

In the makefileurl_launcher_fde_pluginChange toshared_preferences_fde_pluginThis is the makefile needed to compile the plug-in. Just change this name
Change the local cpp file toshared_preferences_fde_plugin.{cc,h}At the same time, the class name and macro should be changed to the corresponding namesedSearch with replace

FLUTTER_PLUGIN_EXPORT void SharedPreferencesRegisterWithRegistrar(
    FlutterDesktopPluginRegistrarRef registrar);

class SharedPreferencesPlugin : public flutter::Plugin {
  virtual ~SharedPreferencesPlugin();
private:
  SharedPreferencesPlugin();
}
...

RegisterWithRegistrarMethod has a channel registered name"plugins.flutter.io/shared_preferences", which is the same as the name when the exception is thrown

void SharedPreferencesPlugin::RegisterWithRegistrar(
    flutter::PluginRegistrar *registrar) {
  auto channel = std::make_unique<flutter::MethodChannel<EncodableValue>>(
      registrar->messenger(), "plugins.flutter.io/shared_preferences",
      &flutter::StandardMethodCodec::GetInstance());
}

In addition, we need to talk about itSharedPreferencesPlugin::HandleMethodCallThis method

void SharedPreferencesPlugin::HandleMethodCall(
    const flutter::MethodCall<EncodableValue> &method_call,
    std::unique_ptr<flutter::MethodResult<EncodableValue>> result) {
}

method_callIt is the method call structure, which contains the name parameter and other information passed from dart layer, and is passed in by reference type;resultIs the method result structure, which contains the return value to be returned to dart layer and the operation result identifier (to identify whether the call is successful) and is passed in as pointer type

The data types of dart and C + + are totally different. How do they transfer each other? This uses a very important data structureflutter::EncodableValue, EncodableValueIn the C + + layer, the data type of dart layer is abstracted. An instance can be used as bool, int, string, map, and list:

Encodeablevalue B (true); // as the encodeablevalue of bool
Encodeablevalue V (32); // as the encodeablevalue of int
Encodeablevalue RET (encodeablevalue:: Type:: Kmap); // as the encodeablevalue of map
Encodeablemap & Map = ret.mapvalue(); // the operation must be converted to encodeablemap type first
std::string key = "some_key";
Map [encodeablevalue (key)] = V; // K / V of encodeablemap must also be encodeablevalue

The fluent engine finally completes the final correspondence of dart type

The plug-in depends on the dart package of the shared preference, so you need to see$FLUTTER_SDK/.pub-cache/hosted/$PUB_HOST/shared_preferences-0.5.3+4/lib/shared_preferences.dartWhat data is passed and needed
The method name used for initialization is’ getall ‘. You need to return all stored key value pairs. You can implement an empty method to pass the compilation phase first:

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();
  }
}

Relational plug-ins

Generate plug-ins

When building an application, the plug-in should also be compiled, so the makefile needs to be modified (note that it’s the makefile of the application, not the plug-in). If the SLN file needs to be modified for windows, in a word, it’s related. After the transformation, theexample/linux/MakefileAs follows:

# 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 [email protected]

$(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)/[email protected]/linux \
        OUT_DIR=$(OUT_DIR) FLUTTER_ROOT=$(FLUTTER_ROOT)

# Plugin library bundling pattern.
$(BUNDLE_LIB_DIR)/%: $(OUT_DIR)/%
    mkdir -p $(BUNDLE_LIB_DIR)
    cp $< [email protected]

$(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 it’s easy to see that the essence is to add a dependency when building an application(ALL_LIBS_OUT), this dependency is some. So files, which are based on our given plug-in directory(FLUTTER_PLUGINS_DIR)Plug in name under(FLUTTER_PLUGIN_NAMES)In the specified directory(OUT_DIRGeneration.

Add plug-ins

After the generation is completed, it needs to be loaded. This load is static, that is to say, it is explicitly called by code at compile timeexample/linux/flutter_embedder_example.ccDiff file for

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;

In this way, it can run. Although there are exceptions in the result, the error message should be “no result”, “but great ~!”, which indicates that the method has been called successfully~
Be carefulDuring compilation, it is better to delete the example / build directory first, so as to generate the latest intermediate files. Otherwise, some strange problems of abnormal exit at runtime may occur due to caching the old generated files

Plug-in implementation

The last step, of course, is how to implement it on the platform layerSharedPreferencesBecause it already hasshared_preferencesDart package, so the implementation of its corresponding interface is good

Name purpose
getAll Return all K / V on initialization
commit Save changes
clear Clear all K / V
remove Remove item
setBool Save bool
setInt Save int

After a search, I found that Linux does not have a widely used K / V repository! Maybe desktop applications have been storing data directly for a long time

Later, I saw flitter go
)Project implementationSharedPreferencesLeveldb is used, so it’s easy to use. It turns out that it’s not easy to use! The key / value of leveldb can be an array of bytes of any length, powerful or powerful, but it’s not appropriate to use it here, because the type information is lost when fetching data, so it’s impossible to know whether the value corresponding to key is int or bool, Unless you design the format of type storage when storing data. It’s too troublesome

Think of the fact that shared ﹣ preference is also an XML file at the bottom of Android, and you need to know the type. At the same time, you don’t need to worry too much about the performance problem at present. Can you just save it in JSON? So you can quickly find the jsoncpp library, which is easy to use, directly operate the file, and you can know the type information of the data after reading it. Perfect!
The ‘getall’ method is as follows:

  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) {

adaptJsonValueMethod simply converts the type of jsoncpp to the type corresponding to the fluent
MoreJson::ValueFor usage, please refer to the previous manual

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);
    }
  }
}

Finally, it is OK to verify the code of dart layer in the flitter project