Flutter can build multiple applications across platforms. If you’re developing an application that needs a desktop version, try the legendary seamless migration.

However, there was a big problem at the beginning: SharedPreferences, widely used on mobile, are only available on the desktop for macOS! Although introducing shared_preferences: [ERROR:flutter/lib/ UI /ui_dart_state.cc(148)] Unhandled Exception: MissingPluginException (No implementation found for method getAll on channel plugins. The flutter. IO/shared_preferences) exception.

This “seamless” came too soon, a little unprepared… Waiting for the official release is definitely not an option. You have to add your own implementation at the platform level. Fortunately, the desktop can be connected to shared_preferences in a plug-in way, combined with the implementation of macOS and the provided sample program finally to work out! Using Linux as an example, write C++ for a long time.

Development environment: Previous attempts to run integrated desktop applications with the latest Flutter1.9 failed, so the development environment is in Flutter1.8, which is sure to work

FlutterSDK: v1.8.0 @ stable

flutter Desktop: c183d46798b9642b8d908710de1e7d14a8573c86@master

pubspec.yaml:

dependencies:
  shared_preferences: ^ 0.5.3 + 4
Copy the code

Run the following command to make sure it works or refer to this article (flutterSDK installation is not explained separately):

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

We developed the SharedPreferences plug-in based on the Example application.

The plug-in structure

All plug-ins are located in plugins in the root directory of the Desktop repository. Flutter_plugins specifically refer to plug-ins that can be used by flutter on other ends (Android /iOS/ Web). The rest represent plug-ins that are only available on the desktop (macOS/ Linux/Windows), The SharedPreferences you need to implement are in plugins/flutter_plugins/shared_preferences_fde, and you can see the macOS only directory. So start creating plug-ins for Linux:

Create directories and files

With urL_launcher_fDE already in place

mkdir -p plugins/flutter_plugins/shared_preferences_fde/linux && cdplugins/flutter_plugins/shared_preferences_fde/linux cp .. /.. /url_launcher_fde/linux/Makefile . cp .. /.. /url_launcher_fde/linux/url_launcher_fde_plugin.{cc,h} .Copy the code

The plugin name

Change the name of the url_launcher_fde_plugin in the Makefile to shared_preferences_fde_plugin, which is the Makefile needed to compile the plug-in. Change the local CPP file to shared_preferences_fde_plugin.{cc,h}, and change the class name and macro name to the appropriate one, preferably with sed search

FLUTTER_PLUGIN_EXPORT void SharedPreferencesRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar); class SharedPreferencesPlugin : public flutter::Plugin { virtual ~SharedPreferencesPlugin(); private: SharedPreferencesPlugin(); }...Copy the code

RegisterWithRegistrar method has a channel in the registered name “plugins. Flutter. IO/shared_preferences”, and this exception is thrown when the name is the same.

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

Also need to specifically say SharedPreferencesPlugin: : HandleMethodCall this method

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

Method_call is a method call structure that contains information such as names and parameters passed from the DART layer as referenced types. Result is the method result structure, which contains the return value that needs to be passed back to the DART layer and the operation result identifier (indicating whether the call was successful), passed in as a pointer.

Dart and c++ have completely different data types. How do they communicate with each other? This uses an important data structure, flutter::EncodableValue. EncodableValue abstracts the dart data type at the c++ layer. An instance of flutter can be a bool, int, String, map, and list:

EncodableValue b(true); // EncodableValue as bool EncodableValue v(32); / / as int EncodableValue EncodableValue ret (EncodableValue: : Type: : kMap); // EncodableValue EncodableMap& map = ret.mapValue (); EncodableMap STD ::string key ="some_key"; map[EncodableValue(key)] = v; // K/V of EncodableMap must also be EncodableValueCopy the code

The Flutter engine finally completes its final correspondence to the DART type.

The plugin relies on shared_preference’s DART package, $FLUTTER_SDK/. Pub -cache/ Hosted /$PUB_HOST/shared_preferences-0.5.3+4/lib/shared_preferences. Initialize with method name ‘getAll’ and need to return all stored key-value pairs, we can first implement an empty method to pass compilation:

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

Associated plug-ins

Generate a plug-in

Example/Linux /Makefile: /Makefile: /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
Copy the code

Diff: add a dependency (ALL_LIBS_OUT) to build your application. This dependency is a.so file. These.so files are generated in the specified directory (OUT_DIR) according to the plugin name (FLUTTER_PLUGIN_NAMES) under the plugin directory we were given (FLUTTER_PLUGINS_DIR).

Load the plug-in

After the build is complete, it needs to be loaded. This load is static, that is, it is explicitly called by code at compile time, directly to the example/ Linux /flutter_embedder_example.cc diff file

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;
Copy the code

The error message should be “no result”, “but great~!” ‘indicates that the method successfully called ~

Note that it is best to remove the example/build directory when compiling, so that the latest intermediate file is generated, otherwise caching the old build file can cause some weird run-time exit problems.

A plugin

The last step, of course, is how to implement the key/value storage of SharedPreferences at the platform level. Since you already have the shared_preferencesDart package, it’s good to implement its corresponding interface.

The name of the use
getAll Returns all k/v on initialization
commit Save the changes
clear Clear all k/ V
remove To remove a
setBool Save a bool
setInt Save the int

Search around and Linux doesn’t have a widely used K/V repository! Probably desktop applications have long had data stored directly in files.

When I saw that levelDB was used for SharedPreferences in the Flutter – Go project, I was glad to use it. LevelDB’s Key/Value can be an array of bytes of any length. LevelDB’s Key/Value can be an array of bytes of any length. LevelDB’s Key/Value can be an array of bytes of any length. It’s too much trouble.

Given that shared_preference is an XML file in android, and you need to know the type, and you don’t have to worry about performance at the moment, why not just store it as JSON? ! So quickly find the jsonCPp library, easy to use, direct operation of files, and after reading can know the type of data information, 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) {Copy the code

The adaptJsonValue method simply converts the type of jsoncpp to the corresponding type of flutter

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();
      returnEncodableValue(v); }}}Copy the code

Finally, the DART layer code in the Flutter project should be verified again.