Dynamic Theme in a Flutter Desktop App

Photo by Vidar Nordli-Mathisen on Unsplash

Synopsis: Learn how to set a WNDPROC callback function from Flutter using Platform Channels in a Plugin. The example shows how to dynamically change the Theme in your App.

Two months ago Flutter 2.10 brought stable Windows support. Ok, this wasn’t news, Flutter compiled applications for Windows before, why is it so important now? The answer is a simple word: “stable”, cause now Google declared it is, we can decide to invest more of our time and resources in developing Apps for Windows using this fantastic tool.

Thanks to the work of several enthusiastic people, a lot of packages on pub.dev support Windows development, and the list is growing. However, as told before, designing our shiny new Windows App in Flutter sometimes there could be something that we miss.

For example, we could need that our App should react to Windows system-wide events, and change its behavior accordingly. Flutter doesn’t have a ready-made interface for doing this. To learn how it can be made we will implement an Event Channel that can make these events available in our Dart code.

If you are already experienced in writing Plugins using Platform Channels, jump to the next paragraph, if not, you can read the previous story:

Suppose we want our App to receive an event every time that the user switches Dark Mode on or off in Windows.

Doing this in a Win32 App, using C++ or other “native” languages, seems straightforward: we have to register an handler to listen for Windows WM_ events that our App receives:

HWND handle = GetActiveWindow();
oldProc = reinterpret_cast<WNDPROC>(GetWindowLongPtr(handle, GWLP_WNDPROC));
SetWindowLongPtr(handle, GWLP_WNDPROC, (LONG_PTR)MyWndProc);

This is our new handler that will be called every time a Windows WM_ message “arrives”:

LRESULT CALLBACK MyWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
if (iMessage == WM_SETTINGCHANGE)
{
if (!lstrcmp(LPCTSTR(lParam), L"ImmersiveColorSet"))
{
ChangeOurAppTheme();
}
}
return oldProc(hWnd, iMessage, wParam, lParam);
}

When a WM_SETTINGCHANGE message is intercepted, if lParam points to “ImmersiveColorSet”` Unicode String, the OS is signaling that something has changed in the current UI Theme.

Being in a Flutter App, things are a little different. Let’s start by cloning this Repository and opening the project in our IDE.

As we know, using a Method Channel in Flutter is like calling a remote API, but now we need to subscribe to a Stream. To do this, we’ll use Event Channelslet’s open lib/windows_dark_mode.dart in our preferred IDE:

Now we are going to add the following code in WindowsDarkMode class:

Doing so, we are:

  1. declaring _eventChannel as an EventChannel instance. This will be our “transport connection” from Dart to Platform code. In the constructor, we pass the channel name. Indeed, we have structured this name as a “path” corresponding to our plugin name and a proper name, that identifies the right stream. I’ve used this convention to better identify the channel and to avoid the risk of collision with other packages, indeed you can name a channel as you want.
  2. implement the static method DarkModeStream() that “translates” the events from the platform. Inside, we call the method receiveBroadcastStream() that provides a Stream<dynamic>the cascading method map()that maps the dynamic value to ThemeModeand distinct(), that debounces events in case two or more consecutive same values ​​should arrive.

Let’s go at “BackEnd”, opening /windows/windows_dark_mode_plugin.cpp :

WindowsDarkModePlugin Already contains the code implemented in my previous tutorial. When asked it provides the current status of Dark Mode.

In RegisterWithRegistrar current we have only the method channel declaration, to receive queries from Dart:

In the same method, we’ll have to implement the Platform event channel:

plugin->m_event_channel = std::make_unique<flutter::EventChannel<flutter::EncodableValue>> (
registrar->messenger (), "windows_dark_mode/dark_mode_callback",
&flutter::StandardMethodCodec::GetInstance ()
);

m_event_channel needs to be declared in class interface, private:

std::unique_ptr<flutter::EventChannel<flutter::EncodableValue>> m_event_channel;

And these include need to be added:

#include <flutter/event_channel.h>
#include <mutex>

The event channel is ready, but to send events it needs a StreamHandler that will enqueue events from platform to engine, so we include in the same namespace our new class:

Declare a variable in WindowsDarkModePlugin interface:

MyStreamHandler<> *m_handler;

Instantiate it, and assign to the event channel in RegisterWithRegistrar,

MyStreamHandler<> *_handler=new MyStreamHandler<> ();
plugin->m_handler = _handler;
auto _obj_stm_handle = static_cast<flutter::StreamHandler<flutter::EncodableValue>*> (plugin->m_handler);
std::unique_ptr<flutter::StreamHandler<flutter::EncodableValue>> _ptr {_obj_stm_handle};
plugin->m_event_channel->SetStreamHandler (std::move (_ptr));

Now we have to intercept WM_ messages. We can call SetWindowLongPtr to set our WNDPROC handler, right?

But we won’t, because Flutter Engine also registered his handler, and it can give us the opportunity to tap in the message flow, so we’ll use RegisterTopLevelWindowProcDelegate , a method provided platform side by Flutter Engine. So we will replace WindowsDarkModePlugin constructor ad destructor:

Now, we have set a callback to hook WM_ messages, and unset it when the WindowsDarkMode’s instance is disposed of.

Then we’ll have to modify the WindowsDarkModePlugin class interface:

  • changing the constructor signature to accept the registrar parameter;
  • declaring int window_proc_id = -1; in the private section, this identifies the handler that we are registering, it will be useful for deregistration;
  • declaring flutter::PluginRegistrarWindows* registrar; in the private section, we need the registrar to unregister our hook when disposing of;
  • declaring std::optional<LRESULT> HandleWindowProc(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam); header in private section;

Then we’ll replace plugin instantiation in RegisterWithRegistrar static method, simply adding registrar parameter:
auto plugin = std::make_unique<WindowsDarkModePlugin>(registrar);

We’ll implement HandleWindowProc in the class body:

We are calling on_callback method on our stream handler, to send the result of isDarkModeAppEnabled() The latter method give us a bool value, but we are boxing it using flutter::EncodableValue()This is the method that we use to send data from platform to Dart Side of our App, where our bool value will be unboxed.

Now we’ll try to compile the project, just to check if the syntax is good so far, if yes….

Now we will modify the main body in “example/lib/main.dart” wrapping our MaterialApp in a StreamBuilder, so our App theme will change when we’ll switch themes in Windows:

Let’s hit Run:

I hope that this tutorial will be useful for you. Soon I will continue on this topic with other tutorials, showing how can debug the C++code of a Plugin in Windows, and how we can get Windows Theme color.

Leave a Comment