C++DartFlutter

Flutter Desktop native Plugins

Neben den neueren FFI Plugins, die Darts ffi Interface nutzen um Shared Libraries zuladen, bietet es auch eine Schnittstelle über Method Channels. Diese nutzen eine bidirektionale Verbindung...

...in der binäre Daten versendet werden können. Dabei kann mit einem Codec das de/kodieren der Nachricht automatisiert werden.

Flutter selbst ist größtenteils plattformagnostisch mittels C++ und Dart realisiert. Es besteht aus mehreren Schichten, die die Abstraktion vom Betriebssystem gewährleisten. Dabei stellt der Embedder die unterste Schicht und gleichzeitig das Bindeglied zwischen der Engine und den Framework auf der einen Seite und den Betriebssystem auf der anderen Seite da.

Jede Plattform hat ihre eigens auf sie abgestimmte Implementation des Embedder. Die verwendete Sprache ist dabei abhängig vom Betriebssystem. Bei Windows ist und Linux ist C++, auf dem Mac OS ist es Objective C, auf iOS Swift und auf Android Kotlin. Für Web als Zielplattform wird eine Kombination aus Web Assembly und JavaScript genutzt.

Im Normalfall nutzt die erstellte Web App für Desktop Systeme die nach Web Assembly kompilierte Skia Engine und für Mobile Dart2Html. Dadurch kann sich die Anwendung unterschiedlich auf Mobile und Desktop Systemen verhalten. Des weiteren werden nicht alle Features im Webbereich unterstützt. Beispielsweise muss man auf den Einsatz von Shadern verzichten. In den meisten Fällen sollte aber eine Web-Implementation dank Web Assembly keine Probleme darstellen.


Plugin Projekt anlegen

Flutter bietet ein Command Line Interface (CLI) zur Verwaltung des Frameworks und Projekten. Um ein neues Projekt aus dem Plugin Template zu erstellen muss folgender Befehl in die Kommandozeile eingegeben werden.

flutter create -t plugin --platforms windows,linux --org michm.de --project-name native_file_lib .\native_file_lib

Als Argument wird der Pfad in dem das Projekt erstellt werden soll erwartet.

Option Beschreibung
-t plugin Als Template wurde Plugin gewählt.
--platforms Die unterstützten Plattformen.
--org Der Name der Organisation. Für App IDs und Package Namen.
--project-name Der Name des Projekts.

Damit wird ein Projekt mit allen nötigen Boilerplate Code zur Kommunikation zwischen Dart Kontext und den in nativen Code implementierten Embedder geschaffen. Wir beschränken uns erst einmal auf Windows und werden uns ggf. auch noch die Linux Variante anschauen.

Verzeichnis Struktur

┌native_file_lib
├──.dart-tool
├─┬example
│ ├⋯
│ ⋮
├─┬lib
│ ├──native_file_lib.dart
│ ├──native_file_lib_method_channel.dart
│ └──native_file_lib_platform_interface.dart
├─┬linux
│ ├─┬include
│ │ └─┬native_file_lib
│ │   └──native_file_lib_plugin.h
│ ├──CMakeList.txt
│ └──native_file_lib_plugin.cc
├──test
├─┬windows
│ ├─┬include
│ │ └─┬native_file_lib
│ │   └──native_file_lib_plugin_c_api.h
│ ├──.gitignore
│ ├──CMakeList.txt
│ ├──native_file_lib_plugin.cpp
│ ├──native_file_lib_plugin.h
│ └──native_file_lib_plugin_c_api.cpp
├──.gitignore
├──.metadata
├──analysis_options.yaml
├──CHANGELOG.md
├──LICENSE
├──native_file_lib.iml
├──pubspec.lock
├──pubspec.yaml
└──README.md

Das Verzeichnis example/ enthält eine sehr ähnlich Verzeichnis - Struktur wie die obere nocheinmal. Allerdings ohne eigenes example/ Verzeichnis.

Da beim Build-Prozess das Plugin in eine Shared Library kompiliert wird, benötigen wir diese Beispiel - Anwendung um die Shared Library später leichter debuggen zu können. Zudem ist nach dem ersten Kompilieren der Example Anwendung, ein neuer Pfad vorhanden. (example/build/windows/)

Dort ist dann ein Visual Studio Projekt drin mit allen nötigen Projekten um die Anwendung erstellen zu können. Dieses werden wir zur Entwicklung des Windows-Parts nutzen.


Die Dart Implementation

Im Ordner native_file_lib/lib/ sind drei Dart Dateien zu finden. Zum einen wäre dort die abstrakte Klasse NativeFileLibPlatform. Sie stellt die Schnittstelle zwischen platformunabhängigen Dart Source Code und plattformabhängigen dar. Sie definiert alle Methoden, die du in deinem Interface zur Verfügung stellen möchtest. Jede einzelne davon wirft allerdings beim Aufruf einen NotImplementedError. Jetzt können wir theoretisch für jede einzelne Plattform eine weitere Klasse mit für die Plattform spezifischen Code aus der NativeFileLibPlatform Klasse ableiten. In der Praxis wird dies an der Stelle aber wohl eher eine Klasse für Web und einzig eine weitere Klasse für jede andere Plattform abgeleitet, da wir uns noch in den Grenzen von Flutter bewegen und dieses, wie Oben schon erwähnt, plattformagnostisch ist. Die Web Schnittstellen und Laufzeitumgebung weicht jedoch von den anderen Implementationen derart ab, dass spezifischer Code nötig sein kann. Dort kann man dann ohne Warnungen zu erhalten JavaScript Interop Module nutzen.

lib/native_file_lib_platform_interface.dart

import 'package:plugin_platform_interface/plugin_platform_interface.dart';

import 'native_file_lib_method_channel.dart';

abstract class NativeFileLibPlatform extends PlatformInterface {
  /// Constructs a MethodChannelsPlatform.
  NativeFileLibPlatform() : super(token: _token);

  static final Object _token = Object();

  static NativeFileLibPlatform _instance = MethodChannelNativeFileLib();

  /// The default instance of [HelloWorldPlatform] to use.
  ///
  /// Defaults to [MethodChannelHelloWorldLib].
  static NativeFileLibPlatform get instance => _instance;

  /// Platform-specific implementations should set this with their own
  /// platform-specific class that extends [MethodChannelsPlatform] when
  /// they register themselves.
  static set instance(NativeFileLibPlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  Future<String?> getPlatformVersion() {
    throw UnimplementedError('platformVersion() has not been implemented.');
  }
  
  /// Saves a file.
  Future<void> saveFile({
    required String filepath,
    required String content,
  }) {
    throw UnimplementedError('saveFile() has not been implemented,');
  }
  
  /// Loads a file
  Future<String?> loadFile(String filepath) {
    throw UnimplementedError('saveFile() has not been implemented,');
  }
}

Die NativeFileLibMethodChannel Klasse stellt eine Implementation der abstrakten Klasse NativeFileLibPlatform dar, die auf Method Channel aufsetzt. Ein Method Channel ist ein Kanal eines bidirektionalen binären Stream, der von Flutter automatisch de-/kodiert werden kann aber nicht muss.

lib/native_file_lib_method_channel.dart

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

import 'native_file_lib_platform_interface.dart';

/// An implementation of [NativeFileLibPlatform] that uses method channels.
class MethodChannelNativeFileLib extends NativeFileLibPlatform {
  /// The method channel used to interact with the native platform.
  @visibleForTesting
  final methodChannel = const MethodChannel('native_file_lib');

  @override
  Future<String?> getPlatformVersion() =>
     methodChannel.invokeMethod<String>('getPlatformVersion');
  
  @override
  Future<void> saveFile({
    required String filepath,
    required String content,
  }) => methodChannel.invokeMethod('save', {
     'filepath': filepath, 
     'content': chontent,
  });
  
  @override
  Future<String?> loadFile(String filepath) => 
     methodChannel.invokeMethod<String>('load', filepath);
}

Die Dart Schnittstelle bildet letztendlich die Klasse NativeFileLib. In ihr sollte nur plattformunabhängiger Dart Code sein. Wenn für die ausführende Plattform keine Implementation zur Verfügung steht, so wird von der abstrakten Elternklasse die Methode aufgerufen in der dann ein UnimplementedError geworfen wird.

lib/native_file_lib.dart

import 'native_file_lib_platform_interface.dart';

class NativeFileLib {
  Future<String?> getPlatformVersion() {
    return NativeFileLibPlatform.instance.getPlatformVersion();
  }
  
  Future<void> saveFile({
    required String filepath,
    required String content,
  }) {
    return NativeFileLibPlatform.instance.saveFile(
      filepath: filepath,
      content: content,
    );
  }
  
  Future<String?> loadFile(String filepath) {
    return NativeFileLibPlatform.instance.loadFile(filepath);
  }
}

Die native Implementation

Die Sprache hier ist abhängig vom Betriebssystem. Für Windows und Linux ist es C++. Allerdings kann je nach dem welche APIs verwendet werden der Code stark unterscheiden.

Wenn du im Unterordner example/ in der Kommandozeile die Flutter CLI mit folgendem Argumenten ausführen, steht dir im Ordner example/build/windows ein Visual Studio Projekt, das du zur Bearbeitung verwenden kannst, zur Verfügung.

flutter build windows

Mit einem Rechtsklick auf das Projekt native_file_lib_example öffnest du in Visual Studio ein Kontextmenü in dem du den Menüpunkt "Als Startprojekt festlegen" findest. Klick auf ihn. Der Name der Projektmappe native_file_lib_example sollte nun fettgedruckt dargstellt werden. Dadurch kannst du die Anwendung aus Visual Studio nun mit und ohne Debugger starten.

Nun benötigen wir zwei zusätzliche Methoden Signaturen in der Klassen-Header-Datei windows/native_file_lib_plugin.h

windows/native_file_lib_plugin.h

#ifndef FLUTTER_PLUGIN_NATIVE_FILE_LIB_PLUGIN_H_
#define FLUTTER_PLUGIN_NATIVE_FILE_LIB_PLUGIN_H_

#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>

#include <memory>

namespace native_file_lib {

class NativeFileLibPlugin : public flutter::Plugin {
 public:
  static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);

  NativeFileLibPlugin();

  virtual ~NativeFileLibPlugin();

  // Disallow copy and assign.
  NativeFileLibPlugin(const NativeFileLibPlugin&) = delete;
  NativeFileLibPlugin& operator=(const NativeFileLibPlugin&) = delete;

 private:
  // Called when a method is called on this plugin's channel from Dart.
  void HandleMethodCall(
      const flutter::MethodCall<flutter::EncodableValue> &method_call,
      std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
      
  // These are the two additional methods, that we need.
  void SaveFile(const std::string &filepath, const std::string &content);
  std::unique_ptr<std::string> LoadFile(const std::string *filepath);
};

}  // namespace native_file_lib

#endif  // FLUTTER_PLUGIN_NATIVE_FILE_LIB_PLUGIN_H_

Die Implementation beider Methoden ist in ein paar Zeilen erledigt. Wir greifen dafür auf Klassen der stdlib zurück und werden noch im scope des native_file_lib Namespace untergebracht.

windows/native_file_lib_plugin.cpp

void NativeFileLibPlugin::SaveFile(const std::string &filepath, const std::string &content) {
    std::ofstream out;
    out.open(filepath, std::ios_base::app);
    out << content.c_str() << std::endl;
    out.close();
}

std::unique_ptr<std::string> NativeFileLibPlugin::LoadFile(const std::string *filepath) {
    std::ifstream in;
    std::string buffer;        
    auto content = std::make_unique<std::string>();
    in.open(filepath->c_str());

    while (std::getline(in, buffer))
        content->append(buffer);
        
    in.close();
    return std::move(content);
}

Die NativeFileLibPlugin::HandleMethodCall Methode benötigt zwei neue Else-If-Zweige. In diesen wollen wir die Übergabeparameter prüfen, konvertieren, die entsprechende Methode aufrufen und je nach dem ein Ergebnis senden oder einen Fehler melden.

windows/native_file_lib_plugin.cpp

void NativeFileLibPlugin::HandleMethodCall(
    const flutter::MethodCall<flutter::EncodableValue> &method_call,
    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
  if (method_call.method_name().compare("getPlatformVersion") == 0) {
    std::ostringstream version_stream;
    version_stream << "Windows ";
    if (IsWindows10OrGreater()) {
      version_stream << "10+";
    } else if (IsWindows8OrGreater()) {
      version_stream << "8";
    } else if (IsWindows7OrGreater()) {
      version_stream << "7";
    }
    result->Success(flutter::EncodableValue(version_stream.str()));
  } else if (method_call.method_name().compare("save") == 0) {
       /* ↓ saving */
      if (auto* args = std::get_if<flutter::EncodableMap>(method_call.arguments())) {
          try {
              const std::string filepath = std::get<std::string>(args->at(flutter::EncodableValue("filepath")));
              const std::string content = std::get<std::string>(args->at(flutter::EncodableValue("content")));
              SaveFile(filepath, content);
              result->Success(flutter::EncodableValue(nullptr));

          } catch (const std::system_error& ex) {
              result->Error(std::to_string(ex.code().value()), ex.what());
          }          
      } else {
          result->Error("0x01", "Invalid arguments: filepath and content are required.");
      }     
      /* ↑ */
  } else if (method_call.method_name().compare("load") == 0) {
      /* ↓ loading */
      if (auto filepath = std::get_if<std::string>(method_call.arguments())) {
          try {
              auto content = LoadFile(filepath);
              result->Success(flutter::EncodableValue(content->c_str()));

          } catch (const std::system_error& ex) {
              result->Error(std::to_string(ex.code().value()), ex.what());
          }
      } else {
          result->Error("0x01", "Invalid arguments: filepath is required.");
      }
      /* ↑ */
  } else {
    result->NotImplemented();
  }
}

Der gleiche Code sollte ohne größere Umstände auch für Linux kompilieren. Vorrausgesetzt man tauscht die Windows spezifischen Includes mit die Linux Varianten aus.


Flutter Beispiel

Das Standard Beispiel passen wir ein wenig an um unsere Native Implementation testen zu können.

example/lib/main.dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:native_file_lib/native_file_lib.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String _content = 'Unknown';
  final _nativeFileLibPlugin = NativeFileLib();

  @override
  void initState() {
    super.initState();
    initPlatformState();
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    String? platformVersion;
    String? content;
    // Platform messages may fail, so we use a try/catch PlatformException.
    // We also handle the message potentially returning null.
    try {
      platformVersion =
          await _nativeFileLibPlugin.getPlatformVersion();
		  
	    content = await _nativeFileLibPlugin.loadFile('test.txt');
    } on PlatformException {
      platformVersion ??= 'unknown platform version';
      content ??= 'Hello World from $platformVersion';
      await _nativeFileLibPlugin.saveFile(
        filepath: 'test.txt',
        content: 'Hello World from $platformVersion',
      );

    }

    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) return;

    if (kDebugMode) print(content);

    setState(() {
      _content = content!;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Center(
          child: Text(_content),
        ),
      ),
    );
  }
}

Repository

Manfred Michaelis / Native File Lib · GitLab
GitLab Community Edition
image