Take advantage of Windows-specific features by writing your Flutter desktop app
Synopsis: Learn to call Windows API from Flutter using Platform Channels in a Plugin. The example shows how to get information from Windows Registry.
Flutter is a gorgeous tool, it let us write Apps that can be compiled and distributed on a growing number of different platforms. To do so, Flutter works by completely abstracting the OS or Hardware layers from our view.
So, when developing our App we “see” only Flutter APIs, we can’t include Assembly Code, use DLLs, call OS APIs, and so on. The Engine has everything under control, of course, it is plenty of functions for interacting with screen and input, for accessing storage, network, and other features that are common between all supported platforms. But several other things are not in-the-box, like location services, motion sensors, Bluetooth, etc.
It is a fair price to pay when we want to distribute an App to every available platform having written it just one time, but what happens if we want more of what destination OS can provide us? Or if we would access that specific hardware or library?
In most cases, we are covered: searching on pub.dev or Github we’ll find a Package that will give us what we need, someone has already solved that problem for us. But sometimes we need more, or we don’t want to depend on a huge package only for a Win32 call.
Flutter offers two ways to let our App to look out of the box:
- dart:ffi, it is a Foreign function interface library, “to call native C APIs, and to read, write, allocate, and deallocate native memory” (from dart.dev). Using only Dart language, it allows us to write Dart code instead of C to call external libraries. However the dart code will be similar to C code in the way we have to manage memory and variables, call external functions, etc.. It also works in pure Dart programs, without Flutter.
- Plugins, using Flutter’s Platform Channels, It’s what we’ll see in this tutorial.
A plugin is “a specialized Dart package that contains an API written in Dart code combined with one or more platform-specific implementations.” Let’s see how it works.
Sorry in advance if sometime this Tutorial can be boring for Dart/Flutter Experts, but this is also an opportunity to learn for beginners.
Suppose we want to know if Dark Mode is activated in Windows, in Flutter it seems easy, we can read the
platformBrightness property of MediaQuery object:
It seems easy, doesn’t it? The property can assume one of two values: dark or light.
But there is a catch: to get a
MediaQuery object, we have to use “.ofService Locator, which looks up in the Widget Tree searching for an instantiated one.
So we have two constraints: we need a context to search upwards, and that context must have a
MediaQuery in his line of parents. The first place where these prerequisites are met is in the
MaterialApp (Or Cupertino, Fluent, etc.) child context.
Suppose that for some reason we need to know if Dark Mode is set before
MateralApp is building, or outside a build at all (into the
Run() method? in a Service? in a Bloc? in a Provider?) in this case we must find another way, we have to sneak out the box where Flutter keeps us in.
We are learning, so for this example, we will leave apart other ready-made solutions (yes, there are packages for that) and find our way.
A rapid search on google reveals us that current theme settings are in Windows Registry:
We see that our desidered value is identified by
AppUseLightTheme valueName, that is located in key:
To get this value, we need to query Win32 API, in this way:
I’m a Stack Overflow ninja (in borrowing code), so it comes from here.
Ok, now that we know where and how, let’s begin to:
In Windows Command Prompt, cd to a folder where we want to create our plugin and:
flutter create --org it.example --template=plugin --platforms=windows windows_dark_mode
Assuming that Flutter SDK is correctly installed and configured, we will obtain a new “windows_dark_mode” folder that contains our plugin stub. Let’s open this project with our IDE, then select “windows” as Target and hit “Run” or “Debug”, this will run the Example Project that Flutter created for us in a subfolder of our package:
It is describing our Windows version, how it works? If we look in “examplelibmain.dart”, we’ll find this in InitState:
Accessing this property we get Platform Version from our OS. Let’s open /lib/windows_dark_mode.dart:
We found here our
WindowsDarkMode class and the
platformVersion getter. No traces of C language or Win32 API there, so let’s try to understand. This is what we see:
- Our example App retrieve Windows Version from
- The getter for this property in turn calls
_channelObject, specifying “getPlatformVersion” in the call;
- _channel is declared as
MethodChannelinstantiated with “windows_dark_mode” channel name.
Given these clues, we can assume that when the named method is invoked from the named channel, This request goes somewhere, and from the same place we’ll receive the answer.
Wait, we already saw this, it’s our everyday job to call remote APIs!
Claro que si! We are calling an API from a “Backend”, and the
MethodChannelis our transport. This is how Platform Channels work, we have a Proxy API on our Dart Side, that calls another Api on Platform Side.
Why someone decided this apparently twisted path, instead of let us include Platform Specific code?
There could be many answers, but I prefer the simpler: Abstraction. In this way, we have our App code that compiles on every platform, because the Dart side is agnostic of the other code on platform side.
If you try to run this example on platforms other than Windows, it runs, thanks to the try-catch block when
platformVersionproperty is accessed in Example’s main.
We can begin to write here, let’s add this method to the
I prefer to put here the
try-catch, so if an error should occur I’ll simply assume that Dark Mode is disabled. It’s normal that this will happen if you run your App using this plugin on platforms other than windows.
Let’s go at “BackEnd”, opening
/windows/windows_dark_mode_plugin.cpp, and focus on what’s important for us:
Here, we have the
WindowsDarkModePlugin class, this is our Platform Side Backend:
RegisterWithRegistrar implementation a method channel is instantiated:
One of the parameter is the name that identifies it. It’s the same name passed to
MethodChannelinstantiation in /lib/windows_dark_mode.dart
Some rows after, the
WindowsDarkModePlugin::HandleMethodCall is set as the handler for this channel. It will be fired when a request is made from another side, like when
platformVersion getter is called in Dart:
WindowsDarkModePlugin::HandleMethodCall method implementation:
As we know, this method will be called every time that a request is put from the other side. It receives a
method_call value, that has a
method_name property. A channel can handle a different kinds of requests so the
method_name identifies the specific request from the “client”. As of now, it recognizes only the
getPlatformVersion request, it is the same string value that we see in Dart client.
The handler returns an
EncodableValue object, constructed around the string to be returned. In Dart the unboxing is implicit, it happens behind the curtains.
Now we’ll begin to implement our new endpoint, so we’ll have to add the code to answer our new request, someone told us (at a time when Flutter’s build method didn’t exist) that functions have to be short, so It’s will be better to refactor this method a little:
We have extracted the
getPlatformVersion() method, and modified
HandleMethodCall() to exit when a method name is matched and related code is executed, now it’s easy to add new calls without chaining more if-else blocks. We can proceed to add the new case:
and adding our new method, that get the desired value from Registry:
We have completed our implementation. We can test it running in our IDE, but before, we have to change the
build method in
examplelibmain.dart, calling our new platform method.
_MyAppState class, declare:
Let’s set its value in
InitStatecalling our newly created property:
Text() Widget initialization in
build method, to show its value:
Text('Running on: $_platformVersionnDark Mode is : $_darkModeEnabled')
Actually, Dark Mode is enabled on this Pc, believe me, if I deactivate it and run again, the App will show “false”.
I hope that this tutorial will be useful for you. Soon I will continue on this topic with other tutorials, showing how we can receive dynamic events from OS, and how we can debug the C++ code of a Plugin in Windows.