diff --git a/package/example/README.md b/package/example/README.md
new file mode 100644
index 000000000..27a38c696
--- /dev/null
+++ b/package/example/README.md
@@ -0,0 +1,7 @@
+# Examples
+
+### My Controls
+
+A library of Flet controls with a testing app.
+
+[Source](my_controls)
diff --git a/package/example/my_controls/.gitignore b/package/example/my_controls/.gitignore
new file mode 100644
index 000000000..ac5aa9893
--- /dev/null
+++ b/package/example/my_controls/.gitignore
@@ -0,0 +1,29 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
+/pubspec.lock
+**/doc/api/
+.dart_tool/
+build/
diff --git a/package/example/my_controls/.metadata b/package/example/my_controls/.metadata
new file mode 100644
index 000000000..07d8623a3
--- /dev/null
+++ b/package/example/my_controls/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: "2e9cb0aa71a386a91f73f7088d115c0d96654829"
+  channel: "stable"
+
+project_type: package
diff --git a/package/example/my_controls/CHANGELOG.md b/package/example/my_controls/CHANGELOG.md
new file mode 100644
index 000000000..4c8732f5e
--- /dev/null
+++ b/package/example/my_controls/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 1.0.0
+
+Initial version of the package.
diff --git a/package/example/my_controls/LICENSE b/package/example/my_controls/LICENSE
new file mode 100644
index 000000000..f49a4e16e
--- /dev/null
+++ b/package/example/my_controls/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
\ No newline at end of file
diff --git a/package/example/my_controls/README.md b/package/example/my_controls/README.md
new file mode 100644
index 000000000..c689bd6b7
--- /dev/null
+++ b/package/example/my_controls/README.md
@@ -0,0 +1,3 @@
+# Flet custom controls example
+
+TODO
\ No newline at end of file
diff --git a/package/example/my_controls/analysis_options.yaml b/package/example/my_controls/analysis_options.yaml
new file mode 100644
index 000000000..a5744c1cf
--- /dev/null
+++ b/package/example/my_controls/analysis_options.yaml
@@ -0,0 +1,4 @@
+include: package:flutter_lints/flutter.yaml
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/package/example/my_controls/example/.gitignore b/package/example/my_controls/example/.gitignore
new file mode 100644
index 000000000..29a3a5017
--- /dev/null
+++ b/package/example/my_controls/example/.gitignore
@@ -0,0 +1,43 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/package/example/my_controls/example/.metadata b/package/example/my_controls/example/.metadata
new file mode 100644
index 000000000..ff67a35e8
--- /dev/null
+++ b/package/example/my_controls/example/.metadata
@@ -0,0 +1,33 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: "2e9cb0aa71a386a91f73f7088d115c0d96654829"
+  channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+  platforms:
+    - platform: root
+      create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
+      base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
+    - platform: macos
+      create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
+      base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
+    - platform: windows
+      create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
+      base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
+
+  # User provided section
+
+  # List of Local paths (relative to this file) that should be
+  # ignored by the migrate tool.
+  #
+  # Files that are not part of the templates will be ignored by default.
+  unmanaged_files:
+    - 'lib/main.dart'
+    - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/package/example/my_controls/example/README.md b/package/example/my_controls/example/README.md
new file mode 100644
index 000000000..ababfb5d7
--- /dev/null
+++ b/package/example/my_controls/example/README.md
@@ -0,0 +1,16 @@
+# my_controls_demo
+
+A new Flutter project.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
+- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
+
+For help getting started with Flutter development, view the
+[online documentation](https://docs.flutter.dev/), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
diff --git a/package/example/my_controls/example/analysis_options.yaml b/package/example/my_controls/example/analysis_options.yaml
new file mode 100644
index 000000000..0d2902135
--- /dev/null
+++ b/package/example/my_controls/example/analysis_options.yaml
@@ -0,0 +1,28 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+  # The lint rules applied to this project can be customized in the
+  # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+  # included above or to enable additional rules. A list of all available lints
+  # and their documentation is published at https://dart.dev/lints.
+  #
+  # Instead of disabling a lint rule for the entire project in the
+  # section below, it can also be suppressed for a single line of code
+  # or a specific dart file by using the `// ignore: name_of_lint` and
+  # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+  # producing the lint.
+  rules:
+    # avoid_print: false  # Uncomment to disable the `avoid_print` rule
+    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/package/example/my_controls/example/lib/main.dart b/package/example/my_controls/example/lib/main.dart
new file mode 100644
index 000000000..8877eafdf
--- /dev/null
+++ b/package/example/my_controls/example/lib/main.dart
@@ -0,0 +1,13 @@
+import 'package:flet/flet.dart';
+import 'package:flutter/material.dart';
+import 'package:my_controls/my_controls.dart' as my_controls;
+
+void main() async {
+  await setupDesktop();
+
+  runApp(FletApp(
+    pageUrl: 'http://localhost:8550',
+    assetsDir: '',
+    createControlFactories: [my_controls.createControl],
+  ));
+}
diff --git a/package/example/my_controls/example/macos/.gitignore b/package/example/my_controls/example/macos/.gitignore
new file mode 100644
index 000000000..746adbb6b
--- /dev/null
+++ b/package/example/my_controls/example/macos/.gitignore
@@ -0,0 +1,7 @@
+# Flutter-related
+**/Flutter/ephemeral/
+**/Pods/
+
+# Xcode-related
+**/dgph
+**/xcuserdata/
diff --git a/package/example/my_controls/example/macos/Flutter/Flutter-Debug.xcconfig b/package/example/my_controls/example/macos/Flutter/Flutter-Debug.xcconfig
new file mode 100644
index 000000000..4b81f9b2d
--- /dev/null
+++ b/package/example/my_controls/example/macos/Flutter/Flutter-Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/package/example/my_controls/example/macos/Flutter/Flutter-Release.xcconfig b/package/example/my_controls/example/macos/Flutter/Flutter-Release.xcconfig
new file mode 100644
index 000000000..5caa9d157
--- /dev/null
+++ b/package/example/my_controls/example/macos/Flutter/Flutter-Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/package/example/my_controls/example/macos/Flutter/GeneratedPluginRegistrant.swift b/package/example/my_controls/example/macos/Flutter/GeneratedPluginRegistrant.swift
new file mode 100644
index 000000000..6b7922d0a
--- /dev/null
+++ b/package/example/my_controls/example/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -0,0 +1,24 @@
+//
+//  Generated file. Do not edit.
+//
+
+import FlutterMacOS
+import Foundation
+
+import audioplayers_darwin
+import path_provider_foundation
+import screen_retriever
+import shared_preferences_foundation
+import url_launcher_macos
+import window_manager
+import window_to_front
+
+func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+  AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
+  PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
+  ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
+  SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
+  UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
+  WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
+  WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin"))
+}
diff --git a/package/example/my_controls/example/macos/Podfile b/package/example/my_controls/example/macos/Podfile
new file mode 100644
index 000000000..c795730db
--- /dev/null
+++ b/package/example/my_controls/example/macos/Podfile
@@ -0,0 +1,43 @@
+platform :osx, '10.14'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+  'Debug' => :debug,
+  'Profile' => :release,
+  'Release' => :release,
+}
+
+def flutter_root
+  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
+  unless File.exist?(generated_xcode_build_settings_path)
+    raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
+  end
+
+  File.foreach(generated_xcode_build_settings_path) do |line|
+    matches = line.match(/FLUTTER_ROOT\=(.*)/)
+    return matches[1].strip if matches
+  end
+  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_macos_podfile_setup
+
+target 'Runner' do
+  use_frameworks!
+  use_modular_headers!
+
+  flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
+  target 'RunnerTests' do
+    inherit! :search_paths
+  end
+end
+
+post_install do |installer|
+  installer.pods_project.targets.each do |target|
+    flutter_additional_macos_build_settings(target)
+  end
+end
diff --git a/package/example/my_controls/example/macos/Podfile.lock b/package/example/my_controls/example/macos/Podfile.lock
new file mode 100644
index 000000000..0c6114e25
--- /dev/null
+++ b/package/example/my_controls/example/macos/Podfile.lock
@@ -0,0 +1,60 @@
+PODS:
+  - audioplayers_darwin (0.0.1):
+    - FlutterMacOS
+  - FlutterMacOS (1.0.0)
+  - path_provider_foundation (0.0.1):
+    - Flutter
+    - FlutterMacOS
+  - screen_retriever (0.0.1):
+    - FlutterMacOS
+  - shared_preferences_foundation (0.0.1):
+    - Flutter
+    - FlutterMacOS
+  - url_launcher_macos (0.0.1):
+    - FlutterMacOS
+  - window_manager (0.2.0):
+    - FlutterMacOS
+  - window_to_front (0.0.1):
+    - FlutterMacOS
+
+DEPENDENCIES:
+  - audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos`)
+  - FlutterMacOS (from `Flutter/ephemeral`)
+  - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
+  - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`)
+  - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
+  - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
+  - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
+  - window_to_front (from `Flutter/ephemeral/.symlinks/plugins/window_to_front/macos`)
+
+EXTERNAL SOURCES:
+  audioplayers_darwin:
+    :path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos
+  FlutterMacOS:
+    :path: Flutter/ephemeral
+  path_provider_foundation:
+    :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
+  screen_retriever:
+    :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos
+  shared_preferences_foundation:
+    :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
+  url_launcher_macos:
+    :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
+  window_manager:
+    :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
+  window_to_front:
+    :path: Flutter/ephemeral/.symlinks/plugins/window_to_front/macos
+
+SPEC CHECKSUMS:
+  audioplayers_darwin: dcad41de4fbd0099cb3749f7ab3b0cb8f70b810c
+  FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
+  path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
+  screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
+  shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
+  url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
+  window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
+  window_to_front: 4cdc24ddd8461ad1a55fa06286d6a79d8b29e8d8
+
+PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
+
+COCOAPODS: 1.14.3
diff --git a/package/example/my_controls/example/macos/Runner.xcodeproj/project.pbxproj b/package/example/my_controls/example/macos/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 000000000..fc9471079
--- /dev/null
+++ b/package/example/my_controls/example/macos/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,791 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 54;
+	objects = {
+
+/* Begin PBXAggregateTarget section */
+		33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
+			isa = PBXAggregateTarget;
+			buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
+			buildPhases = (
+				33CC111E2044C6BF0003C045 /* ShellScript */,
+			);
+			dependencies = (
+			);
+			name = "Flutter Assemble";
+			productName = FLX;
+		};
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+		1F436DF7916E8105F477B616 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 562DBBF5F836E5CEFDACC048 /* Pods_Runner.framework */; };
+		331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
+		335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
+		33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
+		33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
+		33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
+		33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
+		902249D026A61799A7BFBA1A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 555D41E30EE456DB0F08DCCD /* Pods_RunnerTests.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 33CC10EC2044A3C60003C045;
+			remoteInfo = Runner;
+		};
+		33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 33CC111A2044C6BA0003C045;
+			remoteInfo = FLX;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		33CC110E2044A8840003C045 /* Bundle Framework */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+			);
+			name = "Bundle Framework";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
+		333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
+		335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
+		33CC10ED2044A3C60003C045 /* my_controls_demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = my_controls_demo.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+		33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
+		33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
+		33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
+		33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
+		33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
+		33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
+		33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
+		33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
+		33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
+		33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
+		34B725726563F5BC78ABA1A6 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
+		555D41E30EE456DB0F08DCCD /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		562DBBF5F836E5CEFDACC048 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
+		838963C1989C99CB14E19755 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
+		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
+		A45925ACBE920A75209D7F2B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
+		C550727A38D81AB9AA84FABD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
+		D0DA0AC5AA357360A1E81CBE /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
+		F8F87DE9FA7D0C1AB850C561 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		331C80D2294CF70F00263BE5 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				902249D026A61799A7BFBA1A /* Pods_RunnerTests.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		33CC10EA2044A3C60003C045 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1F436DF7916E8105F477B616 /* Pods_Runner.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		331C80D6294CF71000263BE5 /* RunnerTests */ = {
+			isa = PBXGroup;
+			children = (
+				331C80D7294CF71000263BE5 /* RunnerTests.swift */,
+			);
+			path = RunnerTests;
+			sourceTree = "<group>";
+		};
+		33BA886A226E78AF003329D5 /* Configs */ = {
+			isa = PBXGroup;
+			children = (
+				33E5194F232828860026EE4D /* AppInfo.xcconfig */,
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+				333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
+			);
+			path = Configs;
+			sourceTree = "<group>";
+		};
+		33CC10E42044A3C60003C045 = {
+			isa = PBXGroup;
+			children = (
+				33FAB671232836740065AC1E /* Runner */,
+				33CEB47122A05771004F2AC0 /* Flutter */,
+				331C80D6294CF71000263BE5 /* RunnerTests */,
+				33CC10EE2044A3C60003C045 /* Products */,
+				D73912EC22F37F3D000D13A0 /* Frameworks */,
+				896787364530D557D0CEDDF5 /* Pods */,
+			);
+			sourceTree = "<group>";
+		};
+		33CC10EE2044A3C60003C045 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				33CC10ED2044A3C60003C045 /* my_controls_demo.app */,
+				331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		33CC11242044D66E0003C045 /* Resources */ = {
+			isa = PBXGroup;
+			children = (
+				33CC10F22044A3C60003C045 /* Assets.xcassets */,
+				33CC10F42044A3C60003C045 /* MainMenu.xib */,
+				33CC10F72044A3C60003C045 /* Info.plist */,
+			);
+			name = Resources;
+			path = ..;
+			sourceTree = "<group>";
+		};
+		33CEB47122A05771004F2AC0 /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
+				33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
+				33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
+				33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
+			);
+			path = Flutter;
+			sourceTree = "<group>";
+		};
+		33FAB671232836740065AC1E /* Runner */ = {
+			isa = PBXGroup;
+			children = (
+				33CC10F02044A3C60003C045 /* AppDelegate.swift */,
+				33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
+				33E51913231747F40026EE4D /* DebugProfile.entitlements */,
+				33E51914231749380026EE4D /* Release.entitlements */,
+				33CC11242044D66E0003C045 /* Resources */,
+				33BA886A226E78AF003329D5 /* Configs */,
+			);
+			path = Runner;
+			sourceTree = "<group>";
+		};
+		896787364530D557D0CEDDF5 /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				838963C1989C99CB14E19755 /* Pods-Runner.debug.xcconfig */,
+				C550727A38D81AB9AA84FABD /* Pods-Runner.release.xcconfig */,
+				A45925ACBE920A75209D7F2B /* Pods-Runner.profile.xcconfig */,
+				34B725726563F5BC78ABA1A6 /* Pods-RunnerTests.debug.xcconfig */,
+				D0DA0AC5AA357360A1E81CBE /* Pods-RunnerTests.release.xcconfig */,
+				F8F87DE9FA7D0C1AB850C561 /* Pods-RunnerTests.profile.xcconfig */,
+			);
+			name = Pods;
+			path = Pods;
+			sourceTree = "<group>";
+		};
+		D73912EC22F37F3D000D13A0 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				562DBBF5F836E5CEFDACC048 /* Pods_Runner.framework */,
+				555D41E30EE456DB0F08DCCD /* Pods_RunnerTests.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		331C80D4294CF70F00263BE5 /* RunnerTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+			buildPhases = (
+				E46C7DC6CC180D9C56FCE75B /* [CP] Check Pods Manifest.lock */,
+				331C80D1294CF70F00263BE5 /* Sources */,
+				331C80D2294CF70F00263BE5 /* Frameworks */,
+				331C80D3294CF70F00263BE5 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				331C80DA294CF71000263BE5 /* PBXTargetDependency */,
+			);
+			name = RunnerTests;
+			productName = RunnerTests;
+			productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+		33CC10EC2044A3C60003C045 /* Runner */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
+			buildPhases = (
+				EDA849CD90C5AE7B53C8A7D5 /* [CP] Check Pods Manifest.lock */,
+				33CC10E92044A3C60003C045 /* Sources */,
+				33CC10EA2044A3C60003C045 /* Frameworks */,
+				33CC10EB2044A3C60003C045 /* Resources */,
+				33CC110E2044A8840003C045 /* Bundle Framework */,
+				3399D490228B24CF009A79C7 /* ShellScript */,
+				C3985B58A3995385B8C6EA62 /* [CP] Embed Pods Frameworks */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				33CC11202044C79F0003C045 /* PBXTargetDependency */,
+			);
+			name = Runner;
+			productName = Runner;
+			productReference = 33CC10ED2044A3C60003C045 /* my_controls_demo.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		33CC10E52044A3C60003C045 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastSwiftUpdateCheck = 0920;
+				LastUpgradeCheck = 1430;
+				ORGANIZATIONNAME = "";
+				TargetAttributes = {
+					331C80D4294CF70F00263BE5 = {
+						CreatedOnToolsVersion = 14.0;
+						TestTargetID = 33CC10EC2044A3C60003C045;
+					};
+					33CC10EC2044A3C60003C045 = {
+						CreatedOnToolsVersion = 9.2;
+						LastSwiftMigration = 1100;
+						ProvisioningStyle = Automatic;
+						SystemCapabilities = {
+							com.apple.Sandbox = {
+								enabled = 1;
+							};
+						};
+					};
+					33CC111A2044C6BA0003C045 = {
+						CreatedOnToolsVersion = 9.2;
+						ProvisioningStyle = Manual;
+					};
+				};
+			};
+			buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
+			compatibilityVersion = "Xcode 9.3";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 33CC10E42044A3C60003C045;
+			productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				33CC10EC2044A3C60003C045 /* Runner */,
+				331C80D4294CF70F00263BE5 /* RunnerTests */,
+				33CC111A2044C6BA0003C045 /* Flutter Assemble */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		331C80D3294CF70F00263BE5 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		33CC10EB2044A3C60003C045 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
+				33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+		3399D490228B24CF009A79C7 /* ShellScript */ = {
+			isa = PBXShellScriptBuildPhase;
+			alwaysOutOfDate = 1;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+			);
+			outputFileListPaths = (
+			);
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
+		};
+		33CC111E2044C6BF0003C045 /* ShellScript */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+				Flutter/ephemeral/FlutterInputs.xcfilelist,
+			);
+			inputPaths = (
+				Flutter/ephemeral/tripwire,
+			);
+			outputFileListPaths = (
+				Flutter/ephemeral/FlutterOutputs.xcfilelist,
+			);
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
+		};
+		C3985B58A3995385B8C6EA62 /* [CP] Embed Pods Frameworks */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+			);
+			name = "[CP] Embed Pods Frameworks";
+			outputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+		E46C7DC6CC180D9C56FCE75B /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
+		EDA849CD90C5AE7B53C8A7D5 /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		331C80D1294CF70F00263BE5 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		33CC10E92044A3C60003C045 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
+				33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
+				335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 33CC10EC2044A3C60003C045 /* Runner */;
+			targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
+		};
+		33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
+			targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+		33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
+			isa = PBXVariantGroup;
+			children = (
+				33CC10F52044A3C60003C045 /* Base */,
+			);
+			name = MainMenu.xib;
+			path = Runner;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		331C80DB294CF71000263BE5 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 34B725726563F5BC78ABA1A6 /* Pods-RunnerTests.debug.xcconfig */;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CURRENT_PROJECT_VERSION = 1;
+				GENERATE_INFOPLIST_FILE = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.myControlsDemo.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 5.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/my_controls_demo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/my_controls_demo";
+			};
+			name = Debug;
+		};
+		331C80DC294CF71000263BE5 /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = D0DA0AC5AA357360A1E81CBE /* Pods-RunnerTests.release.xcconfig */;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CURRENT_PROJECT_VERSION = 1;
+				GENERATE_INFOPLIST_FILE = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.myControlsDemo.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 5.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/my_controls_demo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/my_controls_demo";
+			};
+			name = Release;
+		};
+		331C80DD294CF71000263BE5 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = F8F87DE9FA7D0C1AB850C561 /* Pods-RunnerTests.profile.xcconfig */;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CURRENT_PROJECT_VERSION = 1;
+				GENERATE_INFOPLIST_FILE = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.myControlsDemo.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 5.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/my_controls_demo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/my_controls_demo";
+			};
+			name = Profile;
+		};
+		338D0CE9231458BD00FA5F75 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CODE_SIGN_IDENTITY = "-";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.14;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = macosx;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+			};
+			name = Profile;
+		};
+		338D0CEA231458BD00FA5F75 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+				);
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Profile;
+		};
+		338D0CEB231458BD00FA5F75 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Manual;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Profile;
+		};
+		33CC10F92044A3C60003C045 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CODE_SIGN_IDENTITY = "-";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.14;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = macosx;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+			};
+			name = Debug;
+		};
+		33CC10FA2044A3C60003C045 /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CODE_SIGN_IDENTITY = "-";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.14;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = macosx;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+			};
+			name = Release;
+		};
+		33CC10FC2044A3C60003C045 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+				);
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Debug;
+		};
+		33CC10FD2044A3C60003C045 /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+				);
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Release;
+		};
+		33CC111C2044C6BA0003C045 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Manual;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Debug;
+		};
+		33CC111D2044C6BA0003C045 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				331C80DB294CF71000263BE5 /* Debug */,
+				331C80DC294CF71000263BE5 /* Release */,
+				331C80DD294CF71000263BE5 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				33CC10F92044A3C60003C045 /* Debug */,
+				33CC10FA2044A3C60003C045 /* Release */,
+				338D0CE9231458BD00FA5F75 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				33CC10FC2044A3C60003C045 /* Debug */,
+				33CC10FD2044A3C60003C045 /* Release */,
+				338D0CEA231458BD00FA5F75 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				33CC111C2044C6BA0003C045 /* Debug */,
+				33CC111D2044C6BA0003C045 /* Release */,
+				338D0CEB231458BD00FA5F75 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 33CC10E52044A3C60003C045 /* Project object */;
+}
diff --git a/package/example/my_controls/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/package/example/my_controls/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 000000000..18d981003
--- /dev/null
+++ b/package/example/my_controls/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/package/example/my_controls/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/package/example/my_controls/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 000000000..163486e0b
--- /dev/null
+++ b/package/example/my_controls/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1430"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "33CC10EC2044A3C60003C045"
+               BuildableName = "my_controls_demo.app"
+               BlueprintName = "Runner"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "33CC10EC2044A3C60003C045"
+            BuildableName = "my_controls_demo.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <Testables>
+         <TestableReference
+            skipped = "NO"
+            parallelizable = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "331C80D4294CF70F00263BE5"
+               BuildableName = "RunnerTests.xctest"
+               BlueprintName = "RunnerTests"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "33CC10EC2044A3C60003C045"
+            BuildableName = "my_controls_demo.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Profile"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "33CC10EC2044A3C60003C045"
+            BuildableName = "my_controls_demo.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/package/example/my_controls/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/package/example/my_controls/example/macos/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 000000000..21a3cc14c
--- /dev/null
+++ b/package/example/my_controls/example/macos/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+   <FileRef
+      location = "group:Pods/Pods.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/package/example/my_controls/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/package/example/my_controls/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 000000000..18d981003
--- /dev/null
+++ b/package/example/my_controls/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/package/example/my_controls/example/macos/Runner/AppDelegate.swift b/package/example/my_controls/example/macos/Runner/AppDelegate.swift
new file mode 100644
index 000000000..d53ef6437
--- /dev/null
+++ b/package/example/my_controls/example/macos/Runner/AppDelegate.swift
@@ -0,0 +1,9 @@
+import Cocoa
+import FlutterMacOS
+
+@NSApplicationMain
+class AppDelegate: FlutterAppDelegate {
+  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+    return true
+  }
+}
diff --git a/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..a2ec33f19
--- /dev/null
+++ b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+  "images" : [
+    {
+      "size" : "16x16",
+      "idiom" : "mac",
+      "filename" : "app_icon_16.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "16x16",
+      "idiom" : "mac",
+      "filename" : "app_icon_32.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "32x32",
+      "idiom" : "mac",
+      "filename" : "app_icon_32.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "32x32",
+      "idiom" : "mac",
+      "filename" : "app_icon_64.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "128x128",
+      "idiom" : "mac",
+      "filename" : "app_icon_128.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "128x128",
+      "idiom" : "mac",
+      "filename" : "app_icon_256.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "256x256",
+      "idiom" : "mac",
+      "filename" : "app_icon_256.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "256x256",
+      "idiom" : "mac",
+      "filename" : "app_icon_512.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "512x512",
+      "idiom" : "mac",
+      "filename" : "app_icon_512.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "512x512",
+      "idiom" : "mac",
+      "filename" : "app_icon_1024.png",
+      "scale" : "2x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}
diff --git a/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
new file mode 100644
index 000000000..82b6f9d9a
Binary files /dev/null and b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ
diff --git a/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
new file mode 100644
index 000000000..13b35eba5
Binary files /dev/null and b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ
diff --git a/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
new file mode 100644
index 000000000..0a3f5fa40
Binary files /dev/null and b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ
diff --git a/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
new file mode 100644
index 000000000..bdb57226d
Binary files /dev/null and b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ
diff --git a/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
new file mode 100644
index 000000000..f083318e0
Binary files /dev/null and b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ
diff --git a/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
new file mode 100644
index 000000000..326c0e72c
Binary files /dev/null and b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ
diff --git a/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
new file mode 100644
index 000000000..2f1632cfd
Binary files /dev/null and b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ
diff --git a/package/example/my_controls/example/macos/Runner/Base.lproj/MainMenu.xib b/package/example/my_controls/example/macos/Runner/Base.lproj/MainMenu.xib
new file mode 100644
index 000000000..80e867a4e
--- /dev/null
+++ b/package/example/my_controls/example/macos/Runner/Base.lproj/MainMenu.xib
@@ -0,0 +1,343 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
+    <dependencies>
+        <deployment identifier="macosx"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
+            <connections>
+                <outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
+            </connections>
+        </customObject>
+        <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
+        <customObject id="-3" userLabel="Application" customClass="NSObject"/>
+        <customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
+            <connections>
+                <outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
+                <outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
+            </connections>
+        </customObject>
+        <customObject id="YLy-65-1bz" customClass="NSFontManager"/>
+        <menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
+            <items>
+                <menuItem title="APP_NAME" id="1Xt-HY-uBw">
+                    <modifierMask key="keyEquivalentModifierMask"/>
+                    <menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
+                        <items>
+                            <menuItem title="About APP_NAME" id="5kV-Vb-QxS">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <connections>
+                                    <action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
+                            <menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
+                            <menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
+                            <menuItem title="Services" id="NMo-om-nkz">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
+                            <menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
+                                <connections>
+                                    <action selector="hide:" target="-1" id="PnN-Uc-m68"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
+                                <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+                                <connections>
+                                    <action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Show All" id="Kd2-mp-pUS">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <connections>
+                                    <action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
+                            <menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
+                                <connections>
+                                    <action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
+                                </connections>
+                            </menuItem>
+                        </items>
+                    </menu>
+                </menuItem>
+                <menuItem title="Edit" id="5QF-Oa-p0T">
+                    <modifierMask key="keyEquivalentModifierMask"/>
+                    <menu key="submenu" title="Edit" id="W48-6f-4Dl">
+                        <items>
+                            <menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
+                                <connections>
+                                    <action selector="undo:" target="-1" id="M6e-cu-g7V"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
+                                <connections>
+                                    <action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
+                            <menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
+                                <connections>
+                                    <action selector="cut:" target="-1" id="YJe-68-I9s"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
+                                <connections>
+                                    <action selector="copy:" target="-1" id="G1f-GL-Joy"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
+                                <connections>
+                                    <action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
+                                <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+                                <connections>
+                                    <action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Delete" id="pa3-QI-u2k">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <connections>
+                                    <action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
+                                <connections>
+                                    <action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
+                            <menuItem title="Find" id="4EN-yA-p0u">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <menu key="submenu" title="Find" id="1b7-l0-nxx">
+                                    <items>
+                                        <menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
+                                            <connections>
+                                                <action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
+                                            <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+                                            <connections>
+                                                <action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
+                                            <connections>
+                                                <action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
+                                            <connections>
+                                                <action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
+                                            <connections>
+                                                <action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
+                                            <connections>
+                                                <action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
+                                            </connections>
+                                        </menuItem>
+                                    </items>
+                                </menu>
+                            </menuItem>
+                            <menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
+                                    <items>
+                                        <menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
+                                            <connections>
+                                                <action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
+                                            <connections>
+                                                <action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
+                                        <menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
+                                            </connections>
+                                        </menuItem>
+                                    </items>
+                                </menu>
+                            </menuItem>
+                            <menuItem title="Substitutions" id="9ic-FL-obx">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
+                                    <items>
+                                        <menuItem title="Show Substitutions" id="z6F-FW-3nz">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
+                                        <menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Smart Quotes" id="hQb-2v-fYv">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Smart Dashes" id="rgM-f4-ycn">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Smart Links" id="cwL-P1-jid">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Data Detectors" id="tRr-pd-1PS">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Text Replacement" id="HFQ-gK-NFA">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
+                                            </connections>
+                                        </menuItem>
+                                    </items>
+                                </menu>
+                            </menuItem>
+                            <menuItem title="Transformations" id="2oI-Rn-ZJC">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <menu key="submenu" title="Transformations" id="c8a-y6-VQd">
+                                    <items>
+                                        <menuItem title="Make Upper Case" id="vmV-6d-7jI">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Make Lower Case" id="d9M-CD-aMd">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Capitalize" id="UEZ-Bs-lqG">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
+                                            </connections>
+                                        </menuItem>
+                                    </items>
+                                </menu>
+                            </menuItem>
+                            <menuItem title="Speech" id="xrE-MZ-jX0">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <menu key="submenu" title="Speech" id="3rS-ZA-NoH">
+                                    <items>
+                                        <menuItem title="Start Speaking" id="Ynk-f8-cLZ">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Stop Speaking" id="Oyz-dy-DGm">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
+                                            </connections>
+                                        </menuItem>
+                                    </items>
+                                </menu>
+                            </menuItem>
+                        </items>
+                    </menu>
+                </menuItem>
+                <menuItem title="View" id="H8h-7b-M4v">
+                    <modifierMask key="keyEquivalentModifierMask"/>
+                    <menu key="submenu" title="View" id="HyV-fh-RgO">
+                        <items>
+                            <menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
+                                <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
+                                <connections>
+                                    <action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
+                                </connections>
+                            </menuItem>
+                        </items>
+                    </menu>
+                </menuItem>
+                <menuItem title="Window" id="aUF-d1-5bR">
+                    <modifierMask key="keyEquivalentModifierMask"/>
+                    <menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
+                        <items>
+                            <menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
+                                <connections>
+                                    <action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Zoom" id="R4o-n2-Eq4">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <connections>
+                                    <action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
+                            <menuItem title="Bring All to Front" id="LE2-aR-0XJ">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <connections>
+                                    <action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
+                                </connections>
+                            </menuItem>
+                        </items>
+                    </menu>
+                </menuItem>
+                <menuItem title="Help" id="EPT-qC-fAb">
+                    <modifierMask key="keyEquivalentModifierMask"/>
+                    <menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
+                </menuItem>
+            </items>
+            <point key="canvasLocation" x="142" y="-258"/>
+        </menu>
+        <window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
+            <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
+            <rect key="contentRect" x="335" y="390" width="800" height="600"/>
+            <rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
+            <view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
+                <rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
+                <autoresizingMask key="autoresizingMask"/>
+            </view>
+        </window>
+    </objects>
+</document>
diff --git a/package/example/my_controls/example/macos/Runner/Configs/AppInfo.xcconfig b/package/example/my_controls/example/macos/Runner/Configs/AppInfo.xcconfig
new file mode 100644
index 000000000..c49c27972
--- /dev/null
+++ b/package/example/my_controls/example/macos/Runner/Configs/AppInfo.xcconfig
@@ -0,0 +1,14 @@
+// Application-level settings for the Runner target.
+//
+// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
+// future. If not, the values below would default to using the project name when this becomes a
+// 'flutter create' template.
+
+// The application's name. By default this is also the title of the Flutter window.
+PRODUCT_NAME = my_controls_demo
+
+// The application's bundle identifier
+PRODUCT_BUNDLE_IDENTIFIER = com.example.myControlsDemo
+
+// The copyright displayed in application information
+PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved.
diff --git a/package/example/my_controls/example/macos/Runner/Configs/Debug.xcconfig b/package/example/my_controls/example/macos/Runner/Configs/Debug.xcconfig
new file mode 100644
index 000000000..36b0fd946
--- /dev/null
+++ b/package/example/my_controls/example/macos/Runner/Configs/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Debug.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/package/example/my_controls/example/macos/Runner/Configs/Release.xcconfig b/package/example/my_controls/example/macos/Runner/Configs/Release.xcconfig
new file mode 100644
index 000000000..dff4f4956
--- /dev/null
+++ b/package/example/my_controls/example/macos/Runner/Configs/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Release.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/package/example/my_controls/example/macos/Runner/Configs/Warnings.xcconfig b/package/example/my_controls/example/macos/Runner/Configs/Warnings.xcconfig
new file mode 100644
index 000000000..42bcbf478
--- /dev/null
+++ b/package/example/my_controls/example/macos/Runner/Configs/Warnings.xcconfig
@@ -0,0 +1,13 @@
+WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
+GCC_WARN_UNDECLARED_SELECTOR = YES
+CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
+CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
+CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
+CLANG_WARN_PRAGMA_PACK = YES
+CLANG_WARN_STRICT_PROTOTYPES = YES
+CLANG_WARN_COMMA = YES
+GCC_WARN_STRICT_SELECTOR_MATCH = YES
+CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
+CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
+GCC_WARN_SHADOW = YES
+CLANG_WARN_UNREACHABLE_CODE = YES
diff --git a/package/example/my_controls/example/macos/Runner/DebugProfile.entitlements b/package/example/my_controls/example/macos/Runner/DebugProfile.entitlements
new file mode 100644
index 000000000..9f56413f3
--- /dev/null
+++ b/package/example/my_controls/example/macos/Runner/DebugProfile.entitlements
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.security.app-sandbox</key>
+	<false/>
+	<key>com.apple.security.cs.allow-jit</key>
+	<true/>
+	<key>com.apple.security.network.server</key>
+	<true/>
+</dict>
+</plist>
diff --git a/package/example/my_controls/example/macos/Runner/Info.plist b/package/example/my_controls/example/macos/Runner/Info.plist
new file mode 100644
index 000000000..4789daa6a
--- /dev/null
+++ b/package/example/my_controls/example/macos/Runner/Info.plist
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIconFile</key>
+	<string></string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>$(FLUTTER_BUILD_NAME)</string>
+	<key>CFBundleVersion</key>
+	<string>$(FLUTTER_BUILD_NUMBER)</string>
+	<key>LSMinimumSystemVersion</key>
+	<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
+	<key>NSHumanReadableCopyright</key>
+	<string>$(PRODUCT_COPYRIGHT)</string>
+	<key>NSMainNibFile</key>
+	<string>MainMenu</string>
+	<key>NSPrincipalClass</key>
+	<string>NSApplication</string>
+</dict>
+</plist>
diff --git a/package/example/my_controls/example/macos/Runner/MainFlutterWindow.swift b/package/example/my_controls/example/macos/Runner/MainFlutterWindow.swift
new file mode 100644
index 000000000..3cc05eb23
--- /dev/null
+++ b/package/example/my_controls/example/macos/Runner/MainFlutterWindow.swift
@@ -0,0 +1,15 @@
+import Cocoa
+import FlutterMacOS
+
+class MainFlutterWindow: NSWindow {
+  override func awakeFromNib() {
+    let flutterViewController = FlutterViewController()
+    let windowFrame = self.frame
+    self.contentViewController = flutterViewController
+    self.setFrame(windowFrame, display: true)
+
+    RegisterGeneratedPlugins(registry: flutterViewController)
+
+    super.awakeFromNib()
+  }
+}
diff --git a/package/example/my_controls/example/macos/Runner/Release.entitlements b/package/example/my_controls/example/macos/Runner/Release.entitlements
new file mode 100644
index 000000000..e89b7f323
--- /dev/null
+++ b/package/example/my_controls/example/macos/Runner/Release.entitlements
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.security.app-sandbox</key>
+	<false/>
+</dict>
+</plist>
diff --git a/package/example/my_controls/example/macos/RunnerTests/RunnerTests.swift b/package/example/my_controls/example/macos/RunnerTests/RunnerTests.swift
new file mode 100644
index 000000000..5418c9f53
--- /dev/null
+++ b/package/example/my_controls/example/macos/RunnerTests/RunnerTests.swift
@@ -0,0 +1,12 @@
+import FlutterMacOS
+import Cocoa
+import XCTest
+
+class RunnerTests: XCTestCase {
+
+  func testExample() {
+    // If you add code to the Runner application, consider adding tests here.
+    // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
+  }
+
+}
diff --git a/package/example/my_controls/example/pubspec.lock b/package/example/my_controls/example/pubspec.lock
new file mode 100644
index 000000000..e75b6769a
--- /dev/null
+++ b/package/example/my_controls/example/pubspec.lock
@@ -0,0 +1,784 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  args:
+    dependency: transitive
+    description:
+      name: args
+      sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.2"
+  async:
+    dependency: transitive
+    description:
+      name: async
+      sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.11.0"
+  audioplayers:
+    dependency: transitive
+    description:
+      name: audioplayers
+      sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.2.1"
+  audioplayers_android:
+    dependency: transitive
+    description:
+      name: audioplayers_android
+      sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.0.3"
+  audioplayers_darwin:
+    dependency: transitive
+    description:
+      name: audioplayers_darwin
+      sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08"
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.0.2"
+  audioplayers_linux:
+    dependency: transitive
+    description:
+      name: audioplayers_linux
+      sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.0"
+  audioplayers_platform_interface:
+    dependency: transitive
+    description:
+      name: audioplayers_platform_interface
+      sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb"
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.1.0"
+  audioplayers_web:
+    dependency: transitive
+    description:
+      name: audioplayers_web
+      sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.1.0"
+  audioplayers_windows:
+    dependency: transitive
+    description:
+      name: audioplayers_windows
+      sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.0"
+  boolean_selector:
+    dependency: transitive
+    description:
+      name: boolean_selector
+      sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.1"
+  characters:
+    dependency: transitive
+    description:
+      name: characters
+      sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
+  clock:
+    dependency: transitive
+    description:
+      name: clock
+      sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.1"
+  collection:
+    dependency: transitive
+    description:
+      name: collection
+      sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.18.0"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.3"
+  cupertino_icons:
+    dependency: "direct main"
+    description:
+      name: cupertino_icons
+      sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.6"
+  equatable:
+    dependency: transitive
+    description:
+      name: equatable
+      sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.5"
+  fake_async:
+    dependency: transitive
+    description:
+      name: fake_async
+      sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.1"
+  ffi:
+    dependency: transitive
+    description:
+      name: ffi
+      sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.0"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.0"
+  file_picker:
+    dependency: transitive
+    description:
+      name: file_picker
+      sha256: "4e42aacde3b993c5947467ab640882c56947d9d27342a5b6f2895b23956954a6"
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.1.1"
+  fixnum:
+    dependency: transitive
+    description:
+      name: fixnum
+      sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.0"
+  fl_chart:
+    dependency: transitive
+    description:
+      name: fl_chart
+      sha256: "5a74434cc83bf64346efb562f1a06eefaf1bcb530dc3d96a104f631a1eff8d79"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.65.0"
+  flet:
+    dependency: "direct main"
+    description:
+      path: "../../.."
+      relative: true
+    source: path
+    version: "0.19.0"
+  flutter:
+    dependency: "direct main"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_highlight:
+    dependency: transitive
+    description:
+      name: flutter_highlight
+      sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.7.0"
+  flutter_lints:
+    dependency: "direct dev"
+    description:
+      name: flutter_lints
+      sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.3"
+  flutter_markdown:
+    dependency: transitive
+    description:
+      name: flutter_markdown
+      sha256: "30088ce826b5b9cfbf9e8bece34c716c8a59fa54461dcae1e4ac01a94639e762"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.6.18+3"
+  flutter_plugin_android_lifecycle:
+    dependency: transitive
+    description:
+      name: flutter_plugin_android_lifecycle
+      sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.17"
+  flutter_redux:
+    dependency: transitive
+    description:
+      name: flutter_redux
+      sha256: "3b20be9e08d0038e1452fbfa1fdb1ea0a7c3738c997734530b3c6d0bb5fcdbdc"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.10.0"
+  flutter_svg:
+    dependency: transitive
+    description:
+      name: flutter_svg
+      sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.9"
+  flutter_test:
+    dependency: "direct dev"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_web_plugins:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  highlight:
+    dependency: transitive
+    description:
+      name: highlight
+      sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.7.0"
+  http:
+    dependency: transitive
+    description:
+      name: http
+      sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.0"
+  http_parser:
+    dependency: transitive
+    description:
+      name: http_parser
+      sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.0.2"
+  js:
+    dependency: transitive
+    description:
+      name: js
+      sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.6.7"
+  lints:
+    dependency: transitive
+    description:
+      name: lints
+      sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.1"
+  logging:
+    dependency: transitive
+    description:
+      name: logging
+      sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.0"
+  markdown:
+    dependency: transitive
+    description:
+      name: markdown
+      sha256: "1b134d9f8ff2da15cb298efe6cd8b7d2a78958c1b00384ebcbdf13fe340a6c90"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.2.1"
+  matcher:
+    dependency: transitive
+    description:
+      name: matcher
+      sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.12.16"
+  material_color_utilities:
+    dependency: transitive
+    description:
+      name: material_color_utilities
+      sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.5.0"
+  meta:
+    dependency: transitive
+    description:
+      name: meta
+      sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.10.0"
+  my_controls:
+    dependency: "direct main"
+    description:
+      path: ".."
+      relative: true
+    source: path
+    version: "1.0.0"
+  path:
+    dependency: transitive
+    description:
+      name: path
+      sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.8.3"
+  path_parsing:
+    dependency: transitive
+    description:
+      name: path_parsing
+      sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.1"
+  path_provider:
+    dependency: transitive
+    description:
+      name: path_provider
+      sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  path_provider_android:
+    dependency: transitive
+    description:
+      name: path_provider_android
+      sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.2"
+  path_provider_foundation:
+    dependency: transitive
+    description:
+      name: path_provider_foundation
+      sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.2"
+  path_provider_linux:
+    dependency: transitive
+    description:
+      name: path_provider_linux
+      sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.1"
+  path_provider_platform_interface:
+    dependency: transitive
+    description:
+      name: path_provider_platform_interface
+      sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  path_provider_windows:
+    dependency: transitive
+    description:
+      name: path_provider_windows
+      sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.1"
+  petitparser:
+    dependency: transitive
+    description:
+      name: petitparser
+      sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.0.2"
+  platform:
+    dependency: transitive
+    description:
+      name: platform
+      sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.4"
+  plugin_platform_interface:
+    dependency: transitive
+    description:
+      name: plugin_platform_interface
+      sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.8"
+  redux:
+    dependency: transitive
+    description:
+      name: redux
+      sha256: "1e86ed5b1a9a717922d0a0ca41f9bf49c1a587d50050e9426fc65b14e85ec4d7"
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.0.0"
+  screen_retriever:
+    dependency: transitive
+    description:
+      name: screen_retriever
+      sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.1.9"
+  sensors_plus:
+    dependency: transitive
+    description:
+      name: sensors_plus
+      sha256: "8e7fa79b4940442bb595bfc0ee9da4af5a22a0fe6ebacc74998245ee9496a82d"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.0.2"
+  sensors_plus_platform_interface:
+    dependency: transitive
+    description:
+      name: sensors_plus_platform_interface
+      sha256: bc472d6cfd622acb4f020e726433ee31788b038056691ba433fec80e448a094f
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.0"
+  shared_preferences:
+    dependency: transitive
+    description:
+      name: shared_preferences
+      sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.2"
+  shared_preferences_android:
+    dependency: transitive
+    description:
+      name: shared_preferences_android
+      sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.1"
+  shared_preferences_foundation:
+    dependency: transitive
+    description:
+      name: shared_preferences_foundation
+      sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.5"
+  shared_preferences_linux:
+    dependency: transitive
+    description:
+      name: shared_preferences_linux
+      sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.2"
+  shared_preferences_platform_interface:
+    dependency: transitive
+    description:
+      name: shared_preferences_platform_interface
+      sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.2"
+  shared_preferences_web:
+    dependency: transitive
+    description:
+      name: shared_preferences_web
+      sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.2"
+  shared_preferences_windows:
+    dependency: transitive
+    description:
+      name: shared_preferences_windows
+      sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.2"
+  sky_engine:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.99"
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.10.0"
+  sprintf:
+    dependency: transitive
+    description:
+      name: sprintf
+      sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.0"
+  stack_trace:
+    dependency: transitive
+    description:
+      name: stack_trace
+      sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.11.1"
+  stream_channel:
+    dependency: transitive
+    description:
+      name: stream_channel
+      sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.0"
+  synchronized:
+    dependency: transitive
+    description:
+      name: synchronized
+      sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.0+1"
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.1"
+  test_api:
+    dependency: transitive
+    description:
+      name: test_api
+      sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.6.1"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.2"
+  url_launcher:
+    dependency: transitive
+    description:
+      name: url_launcher
+      sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.2.4"
+  url_launcher_android:
+    dependency: transitive
+    description:
+      name: url_launcher_android
+      sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f"
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.2.2"
+  url_launcher_ios:
+    dependency: transitive
+    description:
+      name: url_launcher_ios
+      sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03"
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.2.4"
+  url_launcher_linux:
+    dependency: transitive
+    description:
+      name: url_launcher_linux
+      sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.1"
+  url_launcher_macos:
+    dependency: transitive
+    description:
+      name: url_launcher_macos
+      sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.0"
+  url_launcher_platform_interface:
+    dependency: transitive
+    description:
+      name: url_launcher_platform_interface
+      sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.1"
+  url_launcher_web:
+    dependency: transitive
+    description:
+      name: url_launcher_web
+      sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.3"
+  url_launcher_windows:
+    dependency: transitive
+    description:
+      name: url_launcher_windows
+      sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.1"
+  uuid:
+    dependency: transitive
+    description:
+      name: uuid
+      sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.3.3"
+  vector_graphics:
+    dependency: transitive
+    description:
+      name: vector_graphics
+      sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.9+2"
+  vector_graphics_codec:
+    dependency: transitive
+    description:
+      name: vector_graphics_codec
+      sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.9+2"
+  vector_graphics_compiler:
+    dependency: transitive
+    description:
+      name: vector_graphics_compiler
+      sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.9+2"
+  vector_math:
+    dependency: transitive
+    description:
+      name: vector_math
+      sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.4"
+  web:
+    dependency: transitive
+    description:
+      name: web
+      sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.3.0"
+  web_socket_channel:
+    dependency: transitive
+    description:
+      name: web_socket_channel
+      sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.0"
+  webview_flutter:
+    dependency: transitive
+    description:
+      name: webview_flutter
+      sha256: "71e1bfaef41016c8d5954291df5e9f8c6172f1f6ff3af01b5656456ddb11f94c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.4.4"
+  webview_flutter_android:
+    dependency: transitive
+    description:
+      name: webview_flutter_android
+      sha256: "161af93c2abaf94ef2192bffb53a3658b2d721a3bf99b69aa1e47814ee18cc96"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.13.2"
+  webview_flutter_platform_interface:
+    dependency: transitive
+    description:
+      name: webview_flutter_platform_interface
+      sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.10.0"
+  webview_flutter_wkwebview:
+    dependency: transitive
+    description:
+      name: webview_flutter_wkwebview
+      sha256: "4d062ad505390ecef1c4bfb6001cd857a51e00912cc9dfb66edb1886a9ebd80c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.10.2"
+  win32:
+    dependency: transitive
+    description:
+      name: win32
+      sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.2.0"
+  window_manager:
+    dependency: transitive
+    description:
+      name: window_manager
+      sha256: dcc865277f26a7dad263a47d0e405d77e21f12cb71f30333a52710a408690bd7
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.3.7"
+  window_to_front:
+    dependency: transitive
+    description:
+      name: window_to_front
+      sha256: "7aef379752b7190c10479e12b5fd7c0b9d92adc96817d9e96c59937929512aee"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.0.3"
+  xdg_directories:
+    dependency: transitive
+    description:
+      name: xdg_directories
+      sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.4"
+  xml:
+    dependency: transitive
+    description:
+      name: xml
+      sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.5.0"
+sdks:
+  dart: ">=3.2.3 <4.0.0"
+  flutter: ">=3.16.0"
diff --git a/package/example/my_controls/example/pubspec.yaml b/package/example/my_controls/example/pubspec.yaml
new file mode 100644
index 000000000..b4561ae69
--- /dev/null
+++ b/package/example/my_controls/example/pubspec.yaml
@@ -0,0 +1,96 @@
+name: my_controls_demo
+description: "A new Flutter project."
+# The following line prevents the package from being accidentally published to
+# pub.dev using `flutter pub publish`. This is preferred for private packages.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+
+# The following defines the version and build number for your application.
+# A version number is three numbers separated by dots, like 1.2.43
+# followed by an optional build number separated by a +.
+# Both the version and the builder number may be overridden in flutter
+# build by specifying --build-name and --build-number, respectively.
+# In Android, build-name is used as versionName while build-number used as versionCode.
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
+# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+# In Windows, build-name is used as the major, minor, and patch parts
+# of the product and file versions while build-number is used as the build suffix.
+version: 1.0.0+1
+
+environment:
+  sdk: '>=3.2.3 <4.0.0'
+
+# Dependencies specify other packages that your package needs in order to work.
+# To automatically upgrade your package dependencies to the latest versions
+# consider running `flutter pub upgrade --major-versions`. Alternatively,
+# dependencies can be manually updated by changing the version numbers below to
+# the latest version available on pub.dev. To see which dependencies have newer
+# versions available, run `flutter pub outdated`.
+dependencies:
+  flutter:
+    sdk: flutter
+
+
+  # The following adds the Cupertino Icons font to your application.
+  # Use with the CupertinoIcons class for iOS style icons.
+  cupertino_icons: ^1.0.2
+
+  flet:
+    path: ../../../
+
+  my_controls:
+    path: ../
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+  # The "flutter_lints" package below contains a set of recommended lints to
+  # encourage good coding practices. The lint set provided by the package is
+  # activated in the `analysis_options.yaml` file located at the root of your
+  # package. See that file for information about deactivating specific lint
+  # rules and activating additional ones.
+  flutter_lints: ^2.0.0
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter packages.
+flutter:
+
+  # The following line ensures that the Material Icons font is
+  # included with your application, so that you can use the icons in
+  # the material Icons class.
+  uses-material-design: true
+
+  # To add assets to your application, add an assets section, like this:
+  # assets:
+  #   - images/a_dot_burr.jpeg
+  #   - images/a_dot_ham.jpeg
+
+  # An image asset can refer to one or more resolution-specific "variants", see
+  # https://flutter.dev/assets-and-images/#resolution-aware
+
+  # For details regarding adding assets from package dependencies, see
+  # https://flutter.dev/assets-and-images/#from-packages
+
+  # To add custom fonts to your application, add a fonts section here,
+  # in this "flutter" section. Each entry in this list should have a
+  # "family" key with the font family name, and a "fonts" key with a
+  # list giving the asset and other descriptors for the font. For
+  # example:
+  # fonts:
+  #   - family: Schyler
+  #     fonts:
+  #       - asset: fonts/Schyler-Regular.ttf
+  #       - asset: fonts/Schyler-Italic.ttf
+  #         style: italic
+  #   - family: Trajan Pro
+  #     fonts:
+  #       - asset: fonts/TrajanPro.ttf
+  #       - asset: fonts/TrajanPro_Bold.ttf
+  #         weight: 700
+  #
+  # For details regarding fonts from package dependencies,
+  # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/package/example/my_controls/example/windows/.gitignore b/package/example/my_controls/example/windows/.gitignore
new file mode 100644
index 000000000..d492d0d98
--- /dev/null
+++ b/package/example/my_controls/example/windows/.gitignore
@@ -0,0 +1,17 @@
+flutter/ephemeral/
+
+# Visual Studio user-specific files.
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# Visual Studio build-related files.
+x64/
+x86/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
diff --git a/package/example/my_controls/example/windows/CMakeLists.txt b/package/example/my_controls/example/windows/CMakeLists.txt
new file mode 100644
index 000000000..afa61d2e8
--- /dev/null
+++ b/package/example/my_controls/example/windows/CMakeLists.txt
@@ -0,0 +1,108 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.14)
+project(my_controls_demo LANGUAGES CXX)
+
+# The name of the executable created for the application. Change this to change
+# the on-disk name of your application.
+set(BINARY_NAME "my_controls_demo")
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(VERSION 3.14...3.25)
+
+# Define build configuration option.
+get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+if(IS_MULTICONFIG)
+  set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
+    CACHE STRING "" FORCE)
+else()
+  if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+    set(CMAKE_BUILD_TYPE "Debug" CACHE
+      STRING "Flutter build mode" FORCE)
+    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+      "Debug" "Profile" "Release")
+  endif()
+endif()
+# Define settings for the Profile build mode.
+set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
+set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
+set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
+set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
+
+# Use Unicode for all projects.
+add_definitions(-DUNICODE -D_UNICODE)
+
+# Compilation settings that should be applied to most targets.
+#
+# Be cautious about adding new options here, as plugins use this function by
+# default. In most cases, you should add new options to specific targets instead
+# of modifying this function.
+function(APPLY_STANDARD_SETTINGS TARGET)
+  target_compile_features(${TARGET} PUBLIC cxx_std_17)
+  target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
+  target_compile_options(${TARGET} PRIVATE /EHsc)
+  target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
+  target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
+endfunction()
+
+# Flutter library and tool build rules.
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# Application build; see runner/CMakeLists.txt.
+add_subdirectory("runner")
+
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# Support files are copied into place next to the executable, so that it can
+# run in place. This is done instead of making a separate bundle (as on Linux)
+# so that building and running from within Visual Studio will work.
+set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
+# Make the "install" step default, as it's required to run.
+set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+  set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+  COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+  COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+  COMPONENT Runtime)
+
+if(PLUGIN_BUNDLED_LIBRARIES)
+  install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
+    DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+    COMPONENT Runtime)
+endif()
+
+# Copy the native assets provided by the build.dart from all packages.
+set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
+install(DIRECTORY "${NATIVE_ASSETS_DIR}"
+   DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+   COMPONENT Runtime)
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+  file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+  " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+  DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+  CONFIGURATIONS Profile;Release
+  COMPONENT Runtime)
diff --git a/package/example/my_controls/example/windows/flutter/CMakeLists.txt b/package/example/my_controls/example/windows/flutter/CMakeLists.txt
new file mode 100644
index 000000000..903f4899d
--- /dev/null
+++ b/package/example/my_controls/example/windows/flutter/CMakeLists.txt
@@ -0,0 +1,109 @@
+# This file controls Flutter-level build steps. It should not be edited.
+cmake_minimum_required(VERSION 3.14)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
+
+# Set fallback configurations for older versions of the flutter tool.
+if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
+  set(FLUTTER_TARGET_PLATFORM "windows-x64")
+endif()
+
+# === Flutter Library ===
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+  "flutter_export.h"
+  "flutter_windows.h"
+  "flutter_messenger.h"
+  "flutter_plugin_registrar.h"
+  "flutter_texture_registrar.h"
+)
+list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+  "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
+add_dependencies(flutter flutter_assemble)
+
+# === Wrapper ===
+list(APPEND CPP_WRAPPER_SOURCES_CORE
+  "core_implementations.cc"
+  "standard_codec.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
+  "plugin_registrar.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_APP
+  "flutter_engine.cc"
+  "flutter_view_controller.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
+
+# Wrapper sources needed for a plugin.
+add_library(flutter_wrapper_plugin STATIC
+  ${CPP_WRAPPER_SOURCES_CORE}
+  ${CPP_WRAPPER_SOURCES_PLUGIN}
+)
+apply_standard_settings(flutter_wrapper_plugin)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+  POSITION_INDEPENDENT_CODE ON)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+  CXX_VISIBILITY_PRESET hidden)
+target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
+target_include_directories(flutter_wrapper_plugin PUBLIC
+  "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_plugin flutter_assemble)
+
+# Wrapper sources needed for the runner.
+add_library(flutter_wrapper_app STATIC
+  ${CPP_WRAPPER_SOURCES_CORE}
+  ${CPP_WRAPPER_SOURCES_APP}
+)
+apply_standard_settings(flutter_wrapper_app)
+target_link_libraries(flutter_wrapper_app PUBLIC flutter)
+target_include_directories(flutter_wrapper_app PUBLIC
+  "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_app flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
+set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
+add_custom_command(
+  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+    ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
+    ${CPP_WRAPPER_SOURCES_APP}
+    ${PHONY_OUTPUT}
+  COMMAND ${CMAKE_COMMAND} -E env
+    ${FLUTTER_TOOL_ENVIRONMENT}
+    "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
+      ${FLUTTER_TARGET_PLATFORM} $<CONFIG>
+  VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+  "${FLUTTER_LIBRARY}"
+  ${FLUTTER_LIBRARY_HEADERS}
+  ${CPP_WRAPPER_SOURCES_CORE}
+  ${CPP_WRAPPER_SOURCES_PLUGIN}
+  ${CPP_WRAPPER_SOURCES_APP}
+)
diff --git a/package/example/my_controls/example/windows/flutter/generated_plugin_registrant.cc b/package/example/my_controls/example/windows/flutter/generated_plugin_registrant.cc
new file mode 100644
index 000000000..55f8b87a0
--- /dev/null
+++ b/package/example/my_controls/example/windows/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,26 @@
+//
+//  Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+#include <audioplayers_windows/audioplayers_windows_plugin.h>
+#include <screen_retriever/screen_retriever_plugin.h>
+#include <url_launcher_windows/url_launcher_windows.h>
+#include <window_manager/window_manager_plugin.h>
+#include <window_to_front/window_to_front_plugin.h>
+
+void RegisterPlugins(flutter::PluginRegistry* registry) {
+  AudioplayersWindowsPluginRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
+  ScreenRetrieverPluginRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
+  UrlLauncherWindowsRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("UrlLauncherWindows"));
+  WindowManagerPluginRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("WindowManagerPlugin"));
+  WindowToFrontPluginRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("WindowToFrontPlugin"));
+}
diff --git a/package/example/my_controls/example/windows/flutter/generated_plugin_registrant.h b/package/example/my_controls/example/windows/flutter/generated_plugin_registrant.h
new file mode 100644
index 000000000..dc139d85a
--- /dev/null
+++ b/package/example/my_controls/example/windows/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+//  Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include <flutter/plugin_registry.h>
+
+// Registers Flutter plugins.
+void RegisterPlugins(flutter::PluginRegistry* registry);
+
+#endif  // GENERATED_PLUGIN_REGISTRANT_
diff --git a/package/example/my_controls/example/windows/flutter/generated_plugins.cmake b/package/example/my_controls/example/windows/flutter/generated_plugins.cmake
new file mode 100644
index 000000000..eba8ac422
--- /dev/null
+++ b/package/example/my_controls/example/windows/flutter/generated_plugins.cmake
@@ -0,0 +1,28 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+  audioplayers_windows
+  screen_retriever
+  url_launcher_windows
+  window_manager
+  window_to_front
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
+  target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+  list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
+  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
+endforeach(plugin)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
+  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/package/example/my_controls/example/windows/runner/CMakeLists.txt b/package/example/my_controls/example/windows/runner/CMakeLists.txt
new file mode 100644
index 000000000..394917c05
--- /dev/null
+++ b/package/example/my_controls/example/windows/runner/CMakeLists.txt
@@ -0,0 +1,40 @@
+cmake_minimum_required(VERSION 3.14)
+project(runner LANGUAGES CXX)
+
+# Define the application target. To change its name, change BINARY_NAME in the
+# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
+# work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME} WIN32
+  "flutter_window.cpp"
+  "main.cpp"
+  "utils.cpp"
+  "win32_window.cpp"
+  "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+  "Runner.rc"
+  "runner.exe.manifest"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add preprocessor definitions for the build version.
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
+
+# Disable Windows macros that collide with C++ standard library functions.
+target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
+
+# Add dependency libraries and include directories. Add any application-specific
+# dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
+target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
+target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
diff --git a/package/example/my_controls/example/windows/runner/Runner.rc b/package/example/my_controls/example/windows/runner/Runner.rc
new file mode 100644
index 000000000..a77feb905
--- /dev/null
+++ b/package/example/my_controls/example/windows/runner/Runner.rc
@@ -0,0 +1,121 @@
+// Microsoft Visual C++ generated resource script.
+//
+#pragma code_page(65001)
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+    "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+    "#include ""winres.h""\r\n"
+    "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+    "\r\n"
+    "\0"
+END
+
+#endif    // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Icon
+//
+
+// Icon with lowest ID value placed first to ensure application icon
+// remains consistent on all systems.
+IDI_APP_ICON            ICON                    "resources\\app_icon.ico"
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
+#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
+#else
+#define VERSION_AS_NUMBER 1,0,0,0
+#endif
+
+#if defined(FLUTTER_VERSION)
+#define VERSION_AS_STRING FLUTTER_VERSION
+#else
+#define VERSION_AS_STRING "1.0.0"
+#endif
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION VERSION_AS_NUMBER
+ PRODUCTVERSION VERSION_AS_NUMBER
+ FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+ FILEFLAGS VS_FF_DEBUG
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_APP
+ FILESUBTYPE 0x0L
+BEGIN
+    BLOCK "StringFileInfo"
+    BEGIN
+        BLOCK "040904e4"
+        BEGIN
+            VALUE "CompanyName", "com.example" "\0"
+            VALUE "FileDescription", "my_controls_demo" "\0"
+            VALUE "FileVersion", VERSION_AS_STRING "\0"
+            VALUE "InternalName", "my_controls_demo" "\0"
+            VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0"
+            VALUE "OriginalFilename", "my_controls_demo.exe" "\0"
+            VALUE "ProductName", "my_controls_demo" "\0"
+            VALUE "ProductVersion", VERSION_AS_STRING "\0"
+        END
+    END
+    BLOCK "VarFileInfo"
+    BEGIN
+        VALUE "Translation", 0x409, 1252
+    END
+END
+
+#endif    // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif    // not APSTUDIO_INVOKED
diff --git a/package/example/my_controls/example/windows/runner/flutter_window.cpp b/package/example/my_controls/example/windows/runner/flutter_window.cpp
new file mode 100644
index 000000000..955ee3038
--- /dev/null
+++ b/package/example/my_controls/example/windows/runner/flutter_window.cpp
@@ -0,0 +1,71 @@
+#include "flutter_window.h"
+
+#include <optional>
+
+#include "flutter/generated_plugin_registrant.h"
+
+FlutterWindow::FlutterWindow(const flutter::DartProject& project)
+    : project_(project) {}
+
+FlutterWindow::~FlutterWindow() {}
+
+bool FlutterWindow::OnCreate() {
+  if (!Win32Window::OnCreate()) {
+    return false;
+  }
+
+  RECT frame = GetClientArea();
+
+  // The size here must match the window dimensions to avoid unnecessary surface
+  // creation / destruction in the startup path.
+  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
+      frame.right - frame.left, frame.bottom - frame.top, project_);
+  // Ensure that basic setup of the controller was successful.
+  if (!flutter_controller_->engine() || !flutter_controller_->view()) {
+    return false;
+  }
+  RegisterPlugins(flutter_controller_->engine());
+  SetChildContent(flutter_controller_->view()->GetNativeWindow());
+
+  flutter_controller_->engine()->SetNextFrameCallback([&]() {
+    this->Show();
+  });
+
+  // Flutter can complete the first frame before the "show window" callback is
+  // registered. The following call ensures a frame is pending to ensure the
+  // window is shown. It is a no-op if the first frame hasn't completed yet.
+  flutter_controller_->ForceRedraw();
+
+  return true;
+}
+
+void FlutterWindow::OnDestroy() {
+  if (flutter_controller_) {
+    flutter_controller_ = nullptr;
+  }
+
+  Win32Window::OnDestroy();
+}
+
+LRESULT
+FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
+                              WPARAM const wparam,
+                              LPARAM const lparam) noexcept {
+  // Give Flutter, including plugins, an opportunity to handle window messages.
+  if (flutter_controller_) {
+    std::optional<LRESULT> result =
+        flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
+                                                      lparam);
+    if (result) {
+      return *result;
+    }
+  }
+
+  switch (message) {
+    case WM_FONTCHANGE:
+      flutter_controller_->engine()->ReloadSystemFonts();
+      break;
+  }
+
+  return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
+}
diff --git a/package/example/my_controls/example/windows/runner/flutter_window.h b/package/example/my_controls/example/windows/runner/flutter_window.h
new file mode 100644
index 000000000..6da0652f0
--- /dev/null
+++ b/package/example/my_controls/example/windows/runner/flutter_window.h
@@ -0,0 +1,33 @@
+#ifndef RUNNER_FLUTTER_WINDOW_H_
+#define RUNNER_FLUTTER_WINDOW_H_
+
+#include <flutter/dart_project.h>
+#include <flutter/flutter_view_controller.h>
+
+#include <memory>
+
+#include "win32_window.h"
+
+// A window that does nothing but host a Flutter view.
+class FlutterWindow : public Win32Window {
+ public:
+  // Creates a new FlutterWindow hosting a Flutter view running |project|.
+  explicit FlutterWindow(const flutter::DartProject& project);
+  virtual ~FlutterWindow();
+
+ protected:
+  // Win32Window:
+  bool OnCreate() override;
+  void OnDestroy() override;
+  LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
+                         LPARAM const lparam) noexcept override;
+
+ private:
+  // The project to run.
+  flutter::DartProject project_;
+
+  // The Flutter instance hosted by this window.
+  std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
+};
+
+#endif  // RUNNER_FLUTTER_WINDOW_H_
diff --git a/package/example/my_controls/example/windows/runner/main.cpp b/package/example/my_controls/example/windows/runner/main.cpp
new file mode 100644
index 000000000..90c072a2d
--- /dev/null
+++ b/package/example/my_controls/example/windows/runner/main.cpp
@@ -0,0 +1,43 @@
+#include <flutter/dart_project.h>
+#include <flutter/flutter_view_controller.h>
+#include <windows.h>
+
+#include "flutter_window.h"
+#include "utils.h"
+
+int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
+                      _In_ wchar_t *command_line, _In_ int show_command) {
+  // Attach to console when present (e.g., 'flutter run') or create a
+  // new console when running with a debugger.
+  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
+    CreateAndAttachConsole();
+  }
+
+  // Initialize COM, so that it is available for use in the library and/or
+  // plugins.
+  ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
+
+  flutter::DartProject project(L"data");
+
+  std::vector<std::string> command_line_arguments =
+      GetCommandLineArguments();
+
+  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
+
+  FlutterWindow window(project);
+  Win32Window::Point origin(10, 10);
+  Win32Window::Size size(1280, 720);
+  if (!window.Create(L"my_controls_demo", origin, size)) {
+    return EXIT_FAILURE;
+  }
+  window.SetQuitOnClose(true);
+
+  ::MSG msg;
+  while (::GetMessage(&msg, nullptr, 0, 0)) {
+    ::TranslateMessage(&msg);
+    ::DispatchMessage(&msg);
+  }
+
+  ::CoUninitialize();
+  return EXIT_SUCCESS;
+}
diff --git a/package/example/my_controls/example/windows/runner/resource.h b/package/example/my_controls/example/windows/runner/resource.h
new file mode 100644
index 000000000..66a65d1e4
--- /dev/null
+++ b/package/example/my_controls/example/windows/runner/resource.h
@@ -0,0 +1,16 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by Runner.rc
+//
+#define IDI_APP_ICON                    101
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE        102
+#define _APS_NEXT_COMMAND_VALUE         40001
+#define _APS_NEXT_CONTROL_VALUE         1001
+#define _APS_NEXT_SYMED_VALUE           101
+#endif
+#endif
diff --git a/package/example/my_controls/example/windows/runner/resources/app_icon.ico b/package/example/my_controls/example/windows/runner/resources/app_icon.ico
new file mode 100644
index 000000000..c04e20caf
Binary files /dev/null and b/package/example/my_controls/example/windows/runner/resources/app_icon.ico differ
diff --git a/package/example/my_controls/example/windows/runner/runner.exe.manifest b/package/example/my_controls/example/windows/runner/runner.exe.manifest
new file mode 100644
index 000000000..a42ea7687
--- /dev/null
+++ b/package/example/my_controls/example/windows/runner/runner.exe.manifest
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+  <application xmlns="urn:schemas-microsoft-com:asm.v3">
+    <windowsSettings>
+      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
+    </windowsSettings>
+  </application>
+  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+    <application>
+      <!-- Windows 10 and Windows 11 -->
+      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
+      <!-- Windows 8.1 -->
+      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
+      <!-- Windows 8 -->
+      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
+      <!-- Windows 7 -->
+      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
+    </application>
+  </compatibility>
+</assembly>
diff --git a/package/example/my_controls/example/windows/runner/utils.cpp b/package/example/my_controls/example/windows/runner/utils.cpp
new file mode 100644
index 000000000..b2b08734d
--- /dev/null
+++ b/package/example/my_controls/example/windows/runner/utils.cpp
@@ -0,0 +1,65 @@
+#include "utils.h"
+
+#include <flutter_windows.h>
+#include <io.h>
+#include <stdio.h>
+#include <windows.h>
+
+#include <iostream>
+
+void CreateAndAttachConsole() {
+  if (::AllocConsole()) {
+    FILE *unused;
+    if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
+      _dup2(_fileno(stdout), 1);
+    }
+    if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
+      _dup2(_fileno(stdout), 2);
+    }
+    std::ios::sync_with_stdio();
+    FlutterDesktopResyncOutputStreams();
+  }
+}
+
+std::vector<std::string> GetCommandLineArguments() {
+  // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
+  int argc;
+  wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
+  if (argv == nullptr) {
+    return std::vector<std::string>();
+  }
+
+  std::vector<std::string> command_line_arguments;
+
+  // Skip the first argument as it's the binary name.
+  for (int i = 1; i < argc; i++) {
+    command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
+  }
+
+  ::LocalFree(argv);
+
+  return command_line_arguments;
+}
+
+std::string Utf8FromUtf16(const wchar_t* utf16_string) {
+  if (utf16_string == nullptr) {
+    return std::string();
+  }
+  int target_length = ::WideCharToMultiByte(
+      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+      -1, nullptr, 0, nullptr, nullptr)
+    -1; // remove the trailing null character
+  int input_length = (int)wcslen(utf16_string);
+  std::string utf8_string;
+  if (target_length <= 0 || target_length > utf8_string.max_size()) {
+    return utf8_string;
+  }
+  utf8_string.resize(target_length);
+  int converted_length = ::WideCharToMultiByte(
+      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+      input_length, utf8_string.data(), target_length, nullptr, nullptr);
+  if (converted_length == 0) {
+    return std::string();
+  }
+  return utf8_string;
+}
diff --git a/package/example/my_controls/example/windows/runner/utils.h b/package/example/my_controls/example/windows/runner/utils.h
new file mode 100644
index 000000000..3879d5475
--- /dev/null
+++ b/package/example/my_controls/example/windows/runner/utils.h
@@ -0,0 +1,19 @@
+#ifndef RUNNER_UTILS_H_
+#define RUNNER_UTILS_H_
+
+#include <string>
+#include <vector>
+
+// Creates a console for the process, and redirects stdout and stderr to
+// it for both the runner and the Flutter library.
+void CreateAndAttachConsole();
+
+// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
+// encoded in UTF-8. Returns an empty std::string on failure.
+std::string Utf8FromUtf16(const wchar_t* utf16_string);
+
+// Gets the command line arguments passed in as a std::vector<std::string>,
+// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
+std::vector<std::string> GetCommandLineArguments();
+
+#endif  // RUNNER_UTILS_H_
diff --git a/package/example/my_controls/example/windows/runner/win32_window.cpp b/package/example/my_controls/example/windows/runner/win32_window.cpp
new file mode 100644
index 000000000..60608d0fe
--- /dev/null
+++ b/package/example/my_controls/example/windows/runner/win32_window.cpp
@@ -0,0 +1,288 @@
+#include "win32_window.h"
+
+#include <dwmapi.h>
+#include <flutter_windows.h>
+
+#include "resource.h"
+
+namespace {
+
+/// Window attribute that enables dark mode window decorations.
+///
+/// Redefined in case the developer's machine has a Windows SDK older than
+/// version 10.0.22000.0.
+/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
+#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
+#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
+#endif
+
+constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
+
+/// Registry key for app theme preference.
+///
+/// A value of 0 indicates apps should use dark mode. A non-zero or missing
+/// value indicates apps should use light mode.
+constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
+  L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
+constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
+
+// The number of Win32Window objects that currently exist.
+static int g_active_window_count = 0;
+
+using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
+
+// Scale helper to convert logical scaler values to physical using passed in
+// scale factor
+int Scale(int source, double scale_factor) {
+  return static_cast<int>(source * scale_factor);
+}
+
+// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
+// This API is only needed for PerMonitor V1 awareness mode.
+void EnableFullDpiSupportIfAvailable(HWND hwnd) {
+  HMODULE user32_module = LoadLibraryA("User32.dll");
+  if (!user32_module) {
+    return;
+  }
+  auto enable_non_client_dpi_scaling =
+      reinterpret_cast<EnableNonClientDpiScaling*>(
+          GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
+  if (enable_non_client_dpi_scaling != nullptr) {
+    enable_non_client_dpi_scaling(hwnd);
+  }
+  FreeLibrary(user32_module);
+}
+
+}  // namespace
+
+// Manages the Win32Window's window class registration.
+class WindowClassRegistrar {
+ public:
+  ~WindowClassRegistrar() = default;
+
+  // Returns the singleton registrar instance.
+  static WindowClassRegistrar* GetInstance() {
+    if (!instance_) {
+      instance_ = new WindowClassRegistrar();
+    }
+    return instance_;
+  }
+
+  // Returns the name of the window class, registering the class if it hasn't
+  // previously been registered.
+  const wchar_t* GetWindowClass();
+
+  // Unregisters the window class. Should only be called if there are no
+  // instances of the window.
+  void UnregisterWindowClass();
+
+ private:
+  WindowClassRegistrar() = default;
+
+  static WindowClassRegistrar* instance_;
+
+  bool class_registered_ = false;
+};
+
+WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
+
+const wchar_t* WindowClassRegistrar::GetWindowClass() {
+  if (!class_registered_) {
+    WNDCLASS window_class{};
+    window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
+    window_class.lpszClassName = kWindowClassName;
+    window_class.style = CS_HREDRAW | CS_VREDRAW;
+    window_class.cbClsExtra = 0;
+    window_class.cbWndExtra = 0;
+    window_class.hInstance = GetModuleHandle(nullptr);
+    window_class.hIcon =
+        LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
+    window_class.hbrBackground = 0;
+    window_class.lpszMenuName = nullptr;
+    window_class.lpfnWndProc = Win32Window::WndProc;
+    RegisterClass(&window_class);
+    class_registered_ = true;
+  }
+  return kWindowClassName;
+}
+
+void WindowClassRegistrar::UnregisterWindowClass() {
+  UnregisterClass(kWindowClassName, nullptr);
+  class_registered_ = false;
+}
+
+Win32Window::Win32Window() {
+  ++g_active_window_count;
+}
+
+Win32Window::~Win32Window() {
+  --g_active_window_count;
+  Destroy();
+}
+
+bool Win32Window::Create(const std::wstring& title,
+                         const Point& origin,
+                         const Size& size) {
+  Destroy();
+
+  const wchar_t* window_class =
+      WindowClassRegistrar::GetInstance()->GetWindowClass();
+
+  const POINT target_point = {static_cast<LONG>(origin.x),
+                              static_cast<LONG>(origin.y)};
+  HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
+  UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
+  double scale_factor = dpi / 96.0;
+
+  HWND window = CreateWindow(
+      window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
+      Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
+      Scale(size.width, scale_factor), Scale(size.height, scale_factor),
+      nullptr, nullptr, GetModuleHandle(nullptr), this);
+
+  if (!window) {
+    return false;
+  }
+
+  UpdateTheme(window);
+
+  return OnCreate();
+}
+
+bool Win32Window::Show() {
+  return ShowWindow(window_handle_, SW_SHOWNORMAL);
+}
+
+// static
+LRESULT CALLBACK Win32Window::WndProc(HWND const window,
+                                      UINT const message,
+                                      WPARAM const wparam,
+                                      LPARAM const lparam) noexcept {
+  if (message == WM_NCCREATE) {
+    auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
+    SetWindowLongPtr(window, GWLP_USERDATA,
+                     reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
+
+    auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
+    EnableFullDpiSupportIfAvailable(window);
+    that->window_handle_ = window;
+  } else if (Win32Window* that = GetThisFromHandle(window)) {
+    return that->MessageHandler(window, message, wparam, lparam);
+  }
+
+  return DefWindowProc(window, message, wparam, lparam);
+}
+
+LRESULT
+Win32Window::MessageHandler(HWND hwnd,
+                            UINT const message,
+                            WPARAM const wparam,
+                            LPARAM const lparam) noexcept {
+  switch (message) {
+    case WM_DESTROY:
+      window_handle_ = nullptr;
+      Destroy();
+      if (quit_on_close_) {
+        PostQuitMessage(0);
+      }
+      return 0;
+
+    case WM_DPICHANGED: {
+      auto newRectSize = reinterpret_cast<RECT*>(lparam);
+      LONG newWidth = newRectSize->right - newRectSize->left;
+      LONG newHeight = newRectSize->bottom - newRectSize->top;
+
+      SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
+                   newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
+
+      return 0;
+    }
+    case WM_SIZE: {
+      RECT rect = GetClientArea();
+      if (child_content_ != nullptr) {
+        // Size and position the child window.
+        MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
+                   rect.bottom - rect.top, TRUE);
+      }
+      return 0;
+    }
+
+    case WM_ACTIVATE:
+      if (child_content_ != nullptr) {
+        SetFocus(child_content_);
+      }
+      return 0;
+
+    case WM_DWMCOLORIZATIONCOLORCHANGED:
+      UpdateTheme(hwnd);
+      return 0;
+  }
+
+  return DefWindowProc(window_handle_, message, wparam, lparam);
+}
+
+void Win32Window::Destroy() {
+  OnDestroy();
+
+  if (window_handle_) {
+    DestroyWindow(window_handle_);
+    window_handle_ = nullptr;
+  }
+  if (g_active_window_count == 0) {
+    WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
+  }
+}
+
+Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
+  return reinterpret_cast<Win32Window*>(
+      GetWindowLongPtr(window, GWLP_USERDATA));
+}
+
+void Win32Window::SetChildContent(HWND content) {
+  child_content_ = content;
+  SetParent(content, window_handle_);
+  RECT frame = GetClientArea();
+
+  MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
+             frame.bottom - frame.top, true);
+
+  SetFocus(child_content_);
+}
+
+RECT Win32Window::GetClientArea() {
+  RECT frame;
+  GetClientRect(window_handle_, &frame);
+  return frame;
+}
+
+HWND Win32Window::GetHandle() {
+  return window_handle_;
+}
+
+void Win32Window::SetQuitOnClose(bool quit_on_close) {
+  quit_on_close_ = quit_on_close;
+}
+
+bool Win32Window::OnCreate() {
+  // No-op; provided for subclasses.
+  return true;
+}
+
+void Win32Window::OnDestroy() {
+  // No-op; provided for subclasses.
+}
+
+void Win32Window::UpdateTheme(HWND const window) {
+  DWORD light_mode;
+  DWORD light_mode_size = sizeof(light_mode);
+  LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
+                               kGetPreferredBrightnessRegValue,
+                               RRF_RT_REG_DWORD, nullptr, &light_mode,
+                               &light_mode_size);
+
+  if (result == ERROR_SUCCESS) {
+    BOOL enable_dark_mode = light_mode == 0;
+    DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
+                          &enable_dark_mode, sizeof(enable_dark_mode));
+  }
+}
diff --git a/package/example/my_controls/example/windows/runner/win32_window.h b/package/example/my_controls/example/windows/runner/win32_window.h
new file mode 100644
index 000000000..e901dde68
--- /dev/null
+++ b/package/example/my_controls/example/windows/runner/win32_window.h
@@ -0,0 +1,102 @@
+#ifndef RUNNER_WIN32_WINDOW_H_
+#define RUNNER_WIN32_WINDOW_H_
+
+#include <windows.h>
+
+#include <functional>
+#include <memory>
+#include <string>
+
+// A class abstraction for a high DPI-aware Win32 Window. Intended to be
+// inherited from by classes that wish to specialize with custom
+// rendering and input handling
+class Win32Window {
+ public:
+  struct Point {
+    unsigned int x;
+    unsigned int y;
+    Point(unsigned int x, unsigned int y) : x(x), y(y) {}
+  };
+
+  struct Size {
+    unsigned int width;
+    unsigned int height;
+    Size(unsigned int width, unsigned int height)
+        : width(width), height(height) {}
+  };
+
+  Win32Window();
+  virtual ~Win32Window();
+
+  // Creates a win32 window with |title| that is positioned and sized using
+  // |origin| and |size|. New windows are created on the default monitor. Window
+  // sizes are specified to the OS in physical pixels, hence to ensure a
+  // consistent size this function will scale the inputted width and height as
+  // as appropriate for the default monitor. The window is invisible until
+  // |Show| is called. Returns true if the window was created successfully.
+  bool Create(const std::wstring& title, const Point& origin, const Size& size);
+
+  // Show the current window. Returns true if the window was successfully shown.
+  bool Show();
+
+  // Release OS resources associated with window.
+  void Destroy();
+
+  // Inserts |content| into the window tree.
+  void SetChildContent(HWND content);
+
+  // Returns the backing Window handle to enable clients to set icon and other
+  // window properties. Returns nullptr if the window has been destroyed.
+  HWND GetHandle();
+
+  // If true, closing this window will quit the application.
+  void SetQuitOnClose(bool quit_on_close);
+
+  // Return a RECT representing the bounds of the current client area.
+  RECT GetClientArea();
+
+ protected:
+  // Processes and route salient window messages for mouse handling,
+  // size change and DPI. Delegates handling of these to member overloads that
+  // inheriting classes can handle.
+  virtual LRESULT MessageHandler(HWND window,
+                                 UINT const message,
+                                 WPARAM const wparam,
+                                 LPARAM const lparam) noexcept;
+
+  // Called when CreateAndShow is called, allowing subclass window-related
+  // setup. Subclasses should return false if setup fails.
+  virtual bool OnCreate();
+
+  // Called when Destroy is called.
+  virtual void OnDestroy();
+
+ private:
+  friend class WindowClassRegistrar;
+
+  // OS callback called by message pump. Handles the WM_NCCREATE message which
+  // is passed when the non-client area is being created and enables automatic
+  // non-client DPI scaling so that the non-client area automatically
+  // responds to changes in DPI. All other messages are handled by
+  // MessageHandler.
+  static LRESULT CALLBACK WndProc(HWND const window,
+                                  UINT const message,
+                                  WPARAM const wparam,
+                                  LPARAM const lparam) noexcept;
+
+  // Retrieves a class instance pointer for |window|
+  static Win32Window* GetThisFromHandle(HWND const window) noexcept;
+
+  // Update the window frame's theme to match the system theme.
+  static void UpdateTheme(HWND const window);
+
+  bool quit_on_close_ = false;
+
+  // window handle for top level window.
+  HWND window_handle_ = nullptr;
+
+  // window handle for hosted content.
+  HWND child_content_ = nullptr;
+};
+
+#endif  // RUNNER_WIN32_WINDOW_H_
diff --git a/package/example/my_controls/lib/my_controls.dart b/package/example/my_controls/lib/my_controls.dart
new file mode 100644
index 000000000..d558e00b1
--- /dev/null
+++ b/package/example/my_controls/lib/my_controls.dart
@@ -0,0 +1,4 @@
+library my_controls;
+
+export "src/create_control.dart" show createControl;
+export "src/my_control.dart" show MyControl;
diff --git a/package/example/my_controls/lib/src/create_control.dart b/package/example/my_controls/lib/src/create_control.dart
new file mode 100644
index 000000000..615de3023
--- /dev/null
+++ b/package/example/my_controls/lib/src/create_control.dart
@@ -0,0 +1,15 @@
+import 'package:flet/flet.dart';
+
+import 'my_control.dart';
+
+CreateControlFactory createControl = (CreateControlArgs args) {
+  switch (args.control.type) {
+    case "my_org:my_control":
+      return MyControl(
+          control: args.control,
+          children: args.children,
+          parentDisabled: args.parentDisabled);
+    default:
+      return null;
+  }
+};
diff --git a/package/example/my_controls/lib/src/my_control.dart b/package/example/my_controls/lib/src/my_control.dart
new file mode 100644
index 000000000..a75aec98e
--- /dev/null
+++ b/package/example/my_controls/lib/src/my_control.dart
@@ -0,0 +1,43 @@
+import 'package:flet/flet.dart';
+import 'package:flutter/material.dart';
+
+class MyControl extends StatelessWidget {
+  final Control? parent;
+  final Control control;
+  final List<Control> children;
+  final bool parentDisabled;
+
+  const MyControl(
+      {super.key,
+      this.parent,
+      required this.control,
+      required this.children,
+      required this.parentDisabled});
+
+  @override
+  Widget build(BuildContext context) {
+    debugPrint("Card build: ${control.id}");
+
+    var contentCtrls =
+        children.where((c) => c.name == "content" && c.isVisible);
+    bool disabled = control.isDisabled || parentDisabled;
+
+    return constrainedControl(
+        context,
+        Card(
+            elevation: control.attrDouble("elevation"),
+            shape: parseOutlinedBorder(control, "shape"),
+            margin: parseEdgeInsets(control, "margin"),
+            color: HexColor.fromString(
+                Theme.of(context), control.attrString("color", "")!),
+            shadowColor: HexColor.fromString(
+                Theme.of(context), control.attrString("shadowColor", "")!),
+            surfaceTintColor: HexColor.fromString(
+                Theme.of(context), control.attrString("surfaceTintColor", "")!),
+            child: contentCtrls.isNotEmpty
+                ? createControl(control, contentCtrls.first.id, disabled)
+                : null),
+        parent,
+        control);
+  }
+}
diff --git a/package/example/my_controls/pubspec.yaml b/package/example/my_controls/pubspec.yaml
new file mode 100644
index 000000000..3ac9d1b02
--- /dev/null
+++ b/package/example/my_controls/pubspec.yaml
@@ -0,0 +1,21 @@
+name: my_controls
+description: "An example of custom controls for Flet"
+version: 1.0.0
+homepage:
+publish_to: none
+
+environment:
+  sdk: '>=3.2.3 <4.0.0'
+  flutter: ">=1.17.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+
+  flet:
+    path: ../../
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  flutter_lints: ^2.0.0
\ No newline at end of file
diff --git a/package/lib/flet.dart b/package/lib/flet.dart
index 51af540b1..328869fc3 100644
--- a/package/lib/flet.dart
+++ b/package/lib/flet.dart
@@ -1,7 +1,43 @@
 library flet;
 
+export 'src/control_factory.dart';
+export 'src/controls/create_control.dart';
+export 'src/controls/flet_control_stateful_mixin.dart';
+export 'src/controls/flet_control_stateless_mixin.dart';
 export 'src/flet_app.dart';
 export 'src/flet_app_errors_handler.dart';
+export 'src/flet_server.dart';
+export 'src/models/app_state.dart';
+export 'src/models/asset_src.dart';
+export 'src/models/control.dart';
+export 'src/models/control_ancestor_view_model.dart';
+export 'src/models/control_tree_view_model.dart';
+export 'src/models/control_view_model.dart';
+export 'src/models/controls_view_model.dart';
+export 'src/models/page_args_model.dart';
+export 'src/models/page_size_view_model.dart';
 export 'src/utils.dart';
+export 'src/utils/alignment.dart';
+export 'src/utils/animations.dart';
+export 'src/utils/borders.dart';
+export 'src/utils/buttons.dart';
+export 'src/utils/collections.dart';
+export 'src/utils/colors.dart';
+export 'src/utils/dash_path.dart';
+export 'src/utils/debouncer.dart';
+export 'src/utils/drawing.dart';
+export 'src/utils/edge_insets.dart';
+export 'src/utils/gradient.dart';
+export 'src/utils/icons.dart';
+export 'src/utils/images.dart';
+export 'src/utils/material_state.dart';
+export 'src/utils/menu.dart';
+export 'src/utils/mouse.dart';
+export 'src/utils/numbers.dart';
 export 'src/utils/platform_utils_non_web.dart'
     if (dart.library.js) "src/utils/platform_utils_web.dart";
+export 'src/utils/responsive.dart';
+export 'src/utils/shadows.dart';
+export 'src/utils/strings.dart';
+export 'src/utils/text.dart';
+export 'src/utils/textfield.dart';
diff --git a/package/lib/src/control_factory.dart b/package/lib/src/control_factory.dart
new file mode 100644
index 000000000..1612ef377
--- /dev/null
+++ b/package/lib/src/control_factory.dart
@@ -0,0 +1,16 @@
+import 'package:flutter/widgets.dart';
+
+import 'models/control.dart';
+
+class CreateControlArgs {
+  final Key? key;
+  final Control? parent;
+  final Control control;
+  final List<Control> children;
+  final bool parentDisabled;
+
+  CreateControlArgs(
+      this.key, this.parent, this.control, this.children, this.parentDisabled);
+}
+
+typedef CreateControlFactory = Widget? Function(CreateControlArgs args);
diff --git a/package/lib/src/controls/alert_dialog.dart b/package/lib/src/controls/alert_dialog.dart
index 8e71159e4..37703eecc 100644
--- a/package/lib/src/controls/alert_dialog.dart
+++ b/package/lib/src/controls/alert_dialog.dart
@@ -1,18 +1,14 @@
-import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/alignment.dart';
 import '../utils/borders.dart';
 import '../utils/edge_insets.dart';
 import 'create_control.dart';
 import 'cupertino_alert_dialog.dart';
 import 'error.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
 
 class AlertDialogControl extends StatefulWidget {
   final Control? parent;
@@ -21,19 +17,21 @@ class AlertDialogControl extends StatefulWidget {
   final bool parentDisabled;
   final Widget? nextChild;
 
-  const AlertDialogControl(
-      {super.key,
-      this.parent,
-      required this.control,
-      required this.children,
-      required this.parentDisabled,
-      required this.nextChild});
+  const AlertDialogControl({
+    super.key,
+    this.parent,
+    required this.control,
+    required this.children,
+    required this.parentDisabled,
+    required this.nextChild,
+  });
 
   @override
   State<AlertDialogControl> createState() => _AlertDialogControlState();
 }
 
-class _AlertDialogControlState extends State<AlertDialogControl> {
+class _AlertDialogControlState extends State<AlertDialogControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   Widget _createAlertDialog() {
     bool disabled = widget.control.isDisabled || widget.parentDisabled;
     var titleCtrls =
@@ -73,77 +71,64 @@ class _AlertDialogControlState extends State<AlertDialogControl> {
   Widget build(BuildContext context) {
     debugPrint("AlertDialog build ($hashCode): ${widget.control.id}");
 
-    bool adaptive = widget.control.attrBool("adaptive", false)!;
-    if (adaptive &&
-        (defaultTargetPlatform == TargetPlatform.iOS ||
-            defaultTargetPlatform == TargetPlatform.macOS)) {
-      return CupertinoAlertDialogControl(
-        control: widget.control,
-        parentDisabled: widget.parentDisabled,
-        children: widget.children,
-        nextChild: widget.nextChild,
-      );
-    }
-
-    var server = FletAppServices.of(context).server;
-
-    bool lastOpen = widget.control.state["open"] ?? false;
-
-    return StoreConnector<AppState, Function>(
-        distinct: true,
-        converter: (store) => store.dispatch,
-        builder: (context, dispatch) {
-          debugPrint("AlertDialog StoreConnector build: ${widget.control.id}");
-
-          var open = widget.control.attrBool("open", false)!;
-          var modal = widget.control.attrBool("modal", false)!;
-
-          debugPrint("Current open state: $lastOpen");
-          debugPrint("New open state: $open");
-
-          if (open && (open != lastOpen)) {
-            var dialog = _createAlertDialog();
-            if (dialog is ErrorControl) {
-              return dialog;
-            }
-
-            // close previous dialog
-            if (ModalRoute.of(context)?.isCurrent != true) {
-              Navigator.of(context).pop();
+    return withPagePlatform((context, platform) {
+      bool adaptive = widget.control.attrBool("adaptive", false)!;
+      if (adaptive &&
+          (platform == TargetPlatform.iOS ||
+              platform == TargetPlatform.macOS)) {
+        return CupertinoAlertDialogControl(
+          control: widget.control,
+          parentDisabled: widget.parentDisabled,
+          children: widget.children,
+          nextChild: widget.nextChild,
+        );
+      }
+
+      bool lastOpen = widget.control.state["open"] ?? false;
+
+      debugPrint("AlertDialog build: ${widget.control.id}");
+
+      var open = widget.control.attrBool("open", false)!;
+      var modal = widget.control.attrBool("modal", false)!;
+
+      debugPrint("Current open state: $lastOpen");
+      debugPrint("New open state: $open");
+
+      if (open && (open != lastOpen)) {
+        var dialog = _createAlertDialog();
+        if (dialog is ErrorControl) {
+          return dialog;
+        }
+
+        // close previous dialog
+        if (ModalRoute.of(context)?.isCurrent != true) {
+          Navigator.of(context).pop();
+        }
+
+        widget.control.state["open"] = open;
+
+        WidgetsBinding.instance.addPostFrameCallback((_) {
+          showDialog(
+              barrierDismissible: !modal,
+              useRootNavigator: false,
+              context: context,
+              builder: (context) => _createAlertDialog()).then((value) {
+            lastOpen = widget.control.state["open"] ?? false;
+            debugPrint("Dialog should be dismissed ($hashCode): $lastOpen");
+            bool shouldDismiss = lastOpen;
+            widget.control.state["open"] = false;
+
+            if (shouldDismiss) {
+              updateControlProps(widget.control.id, {"open": "false"});
+              sendControlEvent(widget.control.id, "dismiss", "");
             }
-
-            widget.control.state["open"] = open;
-
-            WidgetsBinding.instance.addPostFrameCallback((_) {
-              showDialog(
-                  barrierDismissible: !modal,
-                  useRootNavigator: false,
-                  context: context,
-                  builder: (context) => _createAlertDialog()).then((value) {
-                lastOpen = widget.control.state["open"] ?? false;
-                debugPrint("Dialog should be dismissed ($hashCode): $lastOpen");
-                bool shouldDismiss = lastOpen;
-                widget.control.state["open"] = false;
-
-                if (shouldDismiss) {
-                  List<Map<String, String>> props = [
-                    {"i": widget.control.id, "open": "false"}
-                  ];
-                  dispatch(UpdateControlPropsAction(
-                      UpdateControlPropsPayload(props: props)));
-                  server.updateControlProps(props: props);
-                  server.sendPageEvent(
-                      eventTarget: widget.control.id,
-                      eventName: "dismiss",
-                      eventData: "");
-                }
-              });
-            });
-          } else if (open != lastOpen && lastOpen) {
-            Navigator.of(context).pop();
-          }
-
-          return widget.nextChild ?? const SizedBox.shrink();
+          });
         });
+      } else if (open != lastOpen && lastOpen) {
+        Navigator.of(context).pop();
+      }
+
+      return widget.nextChild ?? const SizedBox.shrink();
+    });
   }
 }
diff --git a/package/lib/src/controls/app_bar.dart b/package/lib/src/controls/app_bar.dart
index a8fe03226..864651e58 100644
--- a/package/lib/src/controls/app_bar.dart
+++ b/package/lib/src/controls/app_bar.dart
@@ -1,12 +1,15 @@
-import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 
 import '../models/control.dart';
 import '../utils/colors.dart';
 import 'create_control.dart';
 import 'cupertino_app_bar.dart';
+import 'flet_control_stateless_mixin.dart';
+import 'flet_store_mixin.dart';
 
-class AppBarControl extends StatelessWidget implements PreferredSizeWidget {
+class AppBarControl extends StatelessWidget
+    with FletControlStatelessMixin, FletStoreMixin
+    implements PreferredSizeWidget {
   final Control? parent;
   final Control control;
   final bool parentDisabled;
@@ -25,51 +28,54 @@ class AppBarControl extends StatelessWidget implements PreferredSizeWidget {
   Widget build(BuildContext context) {
     debugPrint("AppBar build: ${control.id}");
 
-    bool adaptive = control.attrBool("adaptive", false)!;
-    if (adaptive &&
-        (defaultTargetPlatform == TargetPlatform.iOS ||
-            defaultTargetPlatform == TargetPlatform.macOS)) {
-      return CupertinoAppBarControl(
-        control: control,
-        parentDisabled: parentDisabled,
-        children: children,
-        bgcolor: HexColor.fromString(
-            Theme.of(context), control.attrString("bgcolor", "")!));
-    }
+    return withPagePlatform((context, platform) {
+      bool adaptive = control.attrBool("adaptive", false)!;
+      if (adaptive &&
+          (platform == TargetPlatform.iOS ||
+              platform == TargetPlatform.macOS)) {
+        return CupertinoAppBarControl(
+            control: control,
+            parentDisabled: parentDisabled,
+            children: children,
+            bgcolor: HexColor.fromString(
+                Theme.of(context), control.attrString("bgcolor", "")!));
+      }
 
-    var leadingCtrls =
-        children.where((c) => c.name == "leading" && c.isVisible);
-    var titleCtrls = children.where((c) => c.name == "title" && c.isVisible);
-    var actionCtrls = children.where((c) => c.name == "action" && c.isVisible);
+      var leadingCtrls =
+          children.where((c) => c.name == "leading" && c.isVisible);
+      var titleCtrls = children.where((c) => c.name == "title" && c.isVisible);
+      var actionCtrls =
+          children.where((c) => c.name == "action" && c.isVisible);
 
-    var leadingWidth = control.attrDouble("leadingWidth");
-    var elevation = control.attrDouble("elevation");
-    var centerTitle = control.attrBool("centerTitle", false)!;
-    var automaticallyImplyLeading =
-        control.attrBool("automaticallyImplyLeading", true)!;
-    var color = HexColor.fromString(
-        Theme.of(context), control.attrString("color", "")!);
-    var bgcolor = HexColor.fromString(
-        Theme.of(context), control.attrString("bgcolor", "")!);
+      var leadingWidth = control.attrDouble("leadingWidth");
+      var elevation = control.attrDouble("elevation");
+      var centerTitle = control.attrBool("centerTitle", false)!;
+      var automaticallyImplyLeading =
+          control.attrBool("automaticallyImplyLeading", true)!;
+      var color = HexColor.fromString(
+          Theme.of(context), control.attrString("color", "")!);
+      var bgcolor = HexColor.fromString(
+          Theme.of(context), control.attrString("bgcolor", "")!);
 
-    return AppBar(
-      leading: leadingCtrls.isNotEmpty
-          ? createControl(control, leadingCtrls.first.id, control.isDisabled)
-          : null,
-      leadingWidth: leadingWidth,
-      automaticallyImplyLeading: automaticallyImplyLeading,
-      title: titleCtrls.isNotEmpty
-          ? createControl(control, titleCtrls.first.id, control.isDisabled)
-          : null,
-      centerTitle: centerTitle,
-      toolbarHeight: preferredSize.height,
-      foregroundColor: color,
-      backgroundColor: bgcolor,
-      elevation: elevation,
-      actions: actionCtrls
-          .map((c) => createControl(control, c.id, control.isDisabled))
-          .toList(),
-    );
+      return AppBar(
+        leading: leadingCtrls.isNotEmpty
+            ? createControl(control, leadingCtrls.first.id, control.isDisabled)
+            : null,
+        leadingWidth: leadingWidth,
+        automaticallyImplyLeading: automaticallyImplyLeading,
+        title: titleCtrls.isNotEmpty
+            ? createControl(control, titleCtrls.first.id, control.isDisabled)
+            : null,
+        centerTitle: centerTitle,
+        toolbarHeight: preferredSize.height,
+        foregroundColor: color,
+        backgroundColor: bgcolor,
+        elevation: elevation,
+        actions: actionCtrls
+            .map((c) => createControl(control, c.id, control.isDisabled))
+            .toList(),
+      );
+    });
   }
 
   @override
diff --git a/package/lib/src/controls/audio.dart b/package/lib/src/controls/audio.dart
index 60f075b51..629558f36 100644
--- a/package/lib/src/controls/audio.dart
+++ b/package/lib/src/controls/audio.dart
@@ -5,34 +5,30 @@ import 'package:audioplayers/audioplayers.dart';
 import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/widgets.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../flet_app_services.dart';
-import '../flet_server.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/page_args_model.dart';
 import '../utils/images.dart';
 import 'error.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
 
 class AudioControl extends StatefulWidget {
   final Control? parent;
   final Control control;
-  final dynamic dispatch;
   final Widget? nextChild;
 
   const AudioControl(
       {super.key,
       required this.parent,
       required this.control,
-      required this.dispatch,
       required this.nextChild});
 
   @override
   State<AudioControl> createState() => _AudioControlState();
 }
 
-class _AudioControlState extends State<AudioControl> {
+class _AudioControlState extends State<AudioControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   AudioPlayer? player;
   void Function(Duration)? _onDurationChanged;
   void Function(PlayerState)? _onStateChanged;
@@ -44,7 +40,6 @@ class _AudioControlState extends State<AudioControl> {
   StreamSubscription? _onStateChangedSubscription;
   StreamSubscription? _onPositionChangedSubscription;
   StreamSubscription? _onSeekCompleteSubscription;
-  FletServer? _server;
 
   @override
   void initState() {
@@ -86,7 +81,7 @@ class _AudioControlState extends State<AudioControl> {
   void _onRemove() {
     debugPrint("Audio.remove($hashCode)");
     widget.control.state["player"]?.dispose();
-    _server?.controlInvokeMethods.remove(widget.control.id);
+    unsubscribeMethods(widget.control.id);
   }
 
   @override
@@ -120,8 +115,6 @@ class _AudioControlState extends State<AudioControl> {
     bool onPositionChanged =
         widget.control.attrBool("onPositionChanged", false)!;
 
-    var server = FletAppServices.of(context).server;
-
     final String prevSrc = widget.control.state["src"] ?? "";
     final String prevSrcBase64 = widget.control.state["srcBase64"] ?? "";
     final ReleaseMode? prevReleaseMode = widget.control.state["releaseMode"];
@@ -129,147 +122,128 @@ class _AudioControlState extends State<AudioControl> {
     final double? prevBalance = widget.control.state["balance"];
     final double? prevPlaybackRate = widget.control.state["playbackRate"];
 
-    return StoreConnector<AppState, PageArgsModel>(
-        distinct: true,
-        converter: (store) => PageArgsModel.fromStore(store),
-        builder: (context, pageArgs) {
-          _onDurationChanged = (duration) {
-            server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "duration_changed",
-                eventData: duration.inMilliseconds.toString());
-          };
-
-          _onStateChanged = (state) {
-            debugPrint("Audio($hashCode) - state_changed: ${state.name}");
-            server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "state_changed",
-                eventData: state.name.toString());
-          };
+    return withPageArgs((context, pageArgs) {
+      _onDurationChanged = (duration) {
+        sendControlEvent(widget.control.id, "duration_changed",
+            duration.inMilliseconds.toString());
+      };
+
+      _onStateChanged = (state) {
+        debugPrint("Audio($hashCode) - state_changed: ${state.name}");
+        sendControlEvent(
+            widget.control.id, "state_changed", state.name.toString());
+      };
+
+      if (onPositionChanged) {
+        _onPositionChanged = (duration) {
+          sendControlEvent(
+              widget.control.id, "position_changed", duration.toString());
+        };
+      }
 
-          if (onPositionChanged) {
-            _onPositionChanged = (duration) {
-              server.sendPageEvent(
-                  eventTarget: widget.control.id,
-                  eventName: "position_changed",
-                  eventData: duration.toString());
-            };
+      _onSeekComplete = () {
+        sendControlEvent(widget.control.id, "seek_complete", "");
+      };
+
+      () async {
+        debugPrint("Audio ($hashCode) src=$src, prevSrc=$prevSrc");
+        debugPrint(
+            "Audio ($hashCode) srcBase64=$srcBase64, prevSrcBase64=$prevSrcBase64");
+
+        bool srcChanged = false;
+        if (src != "" && src != prevSrc) {
+          widget.control.state["src"] = src;
+          srcChanged = true;
+
+          // URL or file?
+          var assetSrc =
+              getAssetSrc(src, pageArgs.pageUri!, pageArgs.assetsDir);
+          if (assetSrc.isFile) {
+            await player?.setSourceDeviceFile(assetSrc.path);
+          } else {
+            await player?.setSourceUrl(assetSrc.path);
           }
-
-          _onSeekComplete = () {
-            server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "seek_complete",
-                eventData: "");
-          };
-
-          () async {
-            debugPrint("Audio ($hashCode) src=$src, prevSrc=$prevSrc");
-            debugPrint(
-                "Audio ($hashCode) srcBase64=$srcBase64, prevSrcBase64=$prevSrcBase64");
-
-            bool srcChanged = false;
-            if (src != "" && src != prevSrc) {
-              widget.control.state["src"] = src;
-              srcChanged = true;
-
-              // URL or file?
-              var assetSrc =
-                  getAssetSrc(src, pageArgs.pageUri!, pageArgs.assetsDir);
-              if (assetSrc.isFile) {
-                await player?.setSourceDeviceFile(assetSrc.path);
-              } else {
-                await player?.setSourceUrl(assetSrc.path);
-              }
-            } else if (srcBase64 != "" && srcBase64 != prevSrcBase64) {
-              widget.control.state["srcBase64"] = srcBase64;
-              srcChanged = true;
-              await player?.setSourceBytes(base64Decode(srcBase64));
-            }
-
-            if (srcChanged) {
-              debugPrint("Audio.srcChanged!");
-              server.sendPageEvent(
-                  eventTarget: widget.control.id,
-                  eventName: "loaded",
-                  eventData: "");
-            }
-
-            if (releaseMode != null && releaseMode != prevReleaseMode) {
-              debugPrint("Audio.setReleaseMode($releaseMode)");
-              widget.control.state["releaseMode"] = releaseMode;
-              await player?.setReleaseMode(releaseMode);
-            }
-
-            if (volume != null &&
-                volume != prevVolume &&
-                volume >= 0 &&
-                volume <= 1) {
-              widget.control.state["volume"] = volume;
-              debugPrint("Audio.setVolume($volume)");
-              await player?.setVolume(volume);
-            }
-
-            if (playbackRate != null &&
-                playbackRate != prevPlaybackRate &&
-                playbackRate >= 0 &&
-                playbackRate <= 2) {
-              widget.control.state["playbackRate"] = playbackRate;
-              debugPrint("Audio.setPlaybackRate($playbackRate)");
-              await player?.setPlaybackRate(playbackRate);
-            }
-
-            if (!kIsWeb &&
-                balance != null &&
-                balance != prevBalance &&
-                balance >= -1 &&
-                balance <= 1) {
-              widget.control.state["balance"] = balance;
-              debugPrint("Audio.setBalance($balance)");
-              await player?.setBalance(balance);
-            }
-
-            if (srcChanged && autoplay) {
-              debugPrint("Audio.resume($srcChanged, $autoplay)");
+        } else if (srcBase64 != "" && srcBase64 != prevSrcBase64) {
+          widget.control.state["srcBase64"] = srcBase64;
+          srcChanged = true;
+          await player?.setSourceBytes(base64Decode(srcBase64));
+        }
+
+        if (srcChanged) {
+          debugPrint("Audio.srcChanged!");
+          sendControlEvent(widget.control.id, "loaded", "");
+        }
+
+        if (releaseMode != null && releaseMode != prevReleaseMode) {
+          debugPrint("Audio.setReleaseMode($releaseMode)");
+          widget.control.state["releaseMode"] = releaseMode;
+          await player?.setReleaseMode(releaseMode);
+        }
+
+        if (volume != null &&
+            volume != prevVolume &&
+            volume >= 0 &&
+            volume <= 1) {
+          widget.control.state["volume"] = volume;
+          debugPrint("Audio.setVolume($volume)");
+          await player?.setVolume(volume);
+        }
+
+        if (playbackRate != null &&
+            playbackRate != prevPlaybackRate &&
+            playbackRate >= 0 &&
+            playbackRate <= 2) {
+          widget.control.state["playbackRate"] = playbackRate;
+          debugPrint("Audio.setPlaybackRate($playbackRate)");
+          await player?.setPlaybackRate(playbackRate);
+        }
+
+        if (!kIsWeb &&
+            balance != null &&
+            balance != prevBalance &&
+            balance >= -1 &&
+            balance <= 1) {
+          widget.control.state["balance"] = balance;
+          debugPrint("Audio.setBalance($balance)");
+          await player?.setBalance(balance);
+        }
+
+        if (srcChanged && autoplay) {
+          debugPrint("Audio.resume($srcChanged, $autoplay)");
+          await player?.resume();
+        }
+
+        subscribeMethods(widget.control.id, (methodName, args) async {
+          switch (methodName) {
+            case "play":
+              await player?.seek(const Duration(milliseconds: 0));
               await player?.resume();
-            }
-
-            _server = server;
-            _server?.controlInvokeMethods[widget.control.id] =
-                (methodName, args) async {
-              switch (methodName) {
-                case "play":
-                  await player?.seek(const Duration(milliseconds: 0));
-                  await player?.resume();
-                  break;
-                case "resume":
-                  await player?.resume();
-                  break;
-                case "pause":
-                  await player?.pause();
-                  break;
-                case "release":
-                  await player?.release();
-                  break;
-                case "seek":
-                  await player?.seek(Duration(
-                      milliseconds: int.tryParse(args["position"] ?? "") ?? 0));
-                  break;
-                case "get_duration":
-                  return (await player?.getDuration())
-                      ?.inMilliseconds
-                      .toString();
-                case "get_current_position":
-                  return (await player?.getCurrentPosition())
-                      ?.inMilliseconds
-                      .toString();
-              }
-              return null;
-            };
-          }();
-
-          return const SizedBox.shrink();
+              break;
+            case "resume":
+              await player?.resume();
+              break;
+            case "pause":
+              await player?.pause();
+              break;
+            case "release":
+              await player?.release();
+              break;
+            case "seek":
+              await player?.seek(Duration(
+                  milliseconds: int.tryParse(args["position"] ?? "") ?? 0));
+              break;
+            case "get_duration":
+              return (await player?.getDuration())?.inMilliseconds.toString();
+            case "get_current_position":
+              return (await player?.getCurrentPosition())
+                  ?.inMilliseconds
+                  .toString();
+          }
+          return null;
         });
+      }();
+
+      return const SizedBox.shrink();
+    });
   }
 }
diff --git a/package/lib/src/controls/banner.dart b/package/lib/src/controls/banner.dart
index 52337d85e..0f4de50f0 100644
--- a/package/lib/src/controls/banner.dart
+++ b/package/lib/src/controls/banner.dart
@@ -1,7 +1,5 @@
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../models/app_state.dart';
 import '../models/control.dart';
 import '../utils/colors.dart';
 import '../utils/edge_insets.dart';
@@ -65,38 +63,33 @@ class _BannerControlState extends State<BannerControl> {
   Widget build(BuildContext context) {
     debugPrint("Banner build: ${widget.control.id}");
 
-    return StoreConnector<AppState, Function>(
-        distinct: true,
-        converter: (store) => store.dispatch,
-        builder: (context, dispatch) {
-          debugPrint("Banner StoreConnector build: ${widget.control.id}");
+    debugPrint("Banner build: ${widget.control.id}");
 
-          var open = widget.control.attrBool("open", false)!;
+    var open = widget.control.attrBool("open", false)!;
 
-          debugPrint("Current open state: $_open");
-          debugPrint("New open state: $open");
+    debugPrint("Current open state: $_open");
+    debugPrint("New open state: $open");
 
-          if (open && (open != _open)) {
-            var banner = _createBanner();
-            if (banner is ErrorControl) {
-              return banner;
-            }
+    if (open && (open != _open)) {
+      var banner = _createBanner();
+      if (banner is ErrorControl) {
+        return banner;
+      }
 
-            WidgetsBinding.instance.addPostFrameCallback((_) {
-              ScaffoldMessenger.of(context).removeCurrentMaterialBanner();
+      WidgetsBinding.instance.addPostFrameCallback((_) {
+        ScaffoldMessenger.of(context).removeCurrentMaterialBanner();
 
-              ScaffoldMessenger.of(context)
-                  .showMaterialBanner(banner as MaterialBanner);
-            });
-          } else if (open != _open && _open) {
-            WidgetsBinding.instance.addPostFrameCallback((_) {
-              ScaffoldMessenger.of(context).removeCurrentMaterialBanner();
-            });
-          }
+        ScaffoldMessenger.of(context)
+            .showMaterialBanner(banner as MaterialBanner);
+      });
+    } else if (open != _open && _open) {
+      WidgetsBinding.instance.addPostFrameCallback((_) {
+        ScaffoldMessenger.of(context).removeCurrentMaterialBanner();
+      });
+    }
 
-          _open = open;
+    _open = open;
 
-          return widget.nextChild ?? const SizedBox.shrink();
-        });
+    return widget.nextChild ?? const SizedBox.shrink();
   }
 }
diff --git a/package/lib/src/controls/barchart.dart b/package/lib/src/controls/barchart.dart
index c7651f3d2..502b09275 100644
--- a/package/lib/src/controls/barchart.dart
+++ b/package/lib/src/controls/barchart.dart
@@ -1,18 +1,13 @@
 import 'dart:convert';
 
 import 'package:collection/collection.dart';
+import 'package:equatable/equatable.dart';
 import 'package:fl_chart/fl_chart.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_redux/flutter_redux.dart';
+import 'package:redux/redux.dart';
 
-import '../flet_app_services.dart';
 import '../models/app_state.dart';
-import '../models/barchart_event_data.dart';
-import '../models/barchart_group_view_model.dart';
-import '../models/barchart_rod_stack_item_view_model.dart';
-import '../models/barchart_rod_view_model.dart';
-import '../models/barchart_view_model.dart';
-import '../models/chart_axis_view_model.dart';
 import '../models/control.dart';
 import '../utils/animations.dart';
 import '../utils/borders.dart';
@@ -20,7 +15,142 @@ import '../utils/charts.dart';
 import '../utils/colors.dart';
 import '../utils/gradient.dart';
 import '../utils/text.dart';
+import 'charts.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
+
+class BarChartEventData extends Equatable {
+  final String eventType;
+  final int? groupIndex;
+  final int? rodIndex;
+  final int? stackItemIndex;
+
+  const BarChartEventData(
+      {required this.eventType,
+      required this.groupIndex,
+      required this.rodIndex,
+      required this.stackItemIndex});
+
+  Map<String, dynamic> toJson() => <String, dynamic>{
+        'type': eventType,
+        'group_index': groupIndex,
+        'rod_index': rodIndex,
+        'stack_item_index': stackItemIndex
+      };
+
+  @override
+  List<Object?> get props => [eventType, groupIndex, rodIndex, stackItemIndex];
+}
+
+class BarChartGroupViewModel extends Equatable {
+  final Control control;
+  final List<BarChartRodViewModel> barRods;
+
+  const BarChartGroupViewModel({required this.control, required this.barRods});
+
+  static BarChartGroupViewModel fromStore(
+      Store<AppState> store, Control control) {
+    return BarChartGroupViewModel(
+        control: control,
+        barRods: store.state.controls[control.id]!.childIds
+            .map((childId) => store.state.controls[childId])
+            .whereNotNull()
+            .where((c) => c.isVisible)
+            .map((c) => BarChartRodViewModel.fromStore(store, c))
+            .toList());
+  }
+
+  @override
+  List<Object?> get props => [control, barRods];
+}
+
+class BarChartRodStackItemViewModel extends Equatable {
+  final Control control;
+
+  const BarChartRodStackItemViewModel({required this.control});
+
+  static BarChartRodStackItemViewModel fromStore(
+      Store<AppState> store, Control control) {
+    return BarChartRodStackItemViewModel(control: control);
+  }
+
+  @override
+  List<Object?> get props => [control];
+}
+
+class BarChartRodViewModel extends Equatable {
+  final Control control;
+  final List<BarChartRodStackItemViewModel> rodStackItems;
+
+  const BarChartRodViewModel(
+      {required this.control, required this.rodStackItems});
+
+  static BarChartRodViewModel fromStore(
+      Store<AppState> store, Control control) {
+    return BarChartRodViewModel(
+        control: control,
+        rodStackItems: store.state.controls[control.id]!.childIds
+            .map((childId) => store.state.controls[childId])
+            .whereNotNull()
+            .where((c) => c.isVisible)
+            .map((c) => BarChartRodStackItemViewModel.fromStore(store, c))
+            .toList());
+  }
+
+  @override
+  List<Object?> get props => [control, rodStackItems];
+}
+
+class BarChartViewModel extends Equatable {
+  final Control control;
+  final ChartAxisViewModel? leftAxis;
+  final ChartAxisViewModel? topAxis;
+  final ChartAxisViewModel? rightAxis;
+  final ChartAxisViewModel? bottomAxis;
+  final List<BarChartGroupViewModel> barGroups;
+
+  const BarChartViewModel(
+      {required this.control,
+      required this.leftAxis,
+      required this.topAxis,
+      required this.rightAxis,
+      required this.bottomAxis,
+      required this.barGroups});
+
+  static BarChartViewModel fromStore(
+      Store<AppState> store, Control control, List<Control> children) {
+    var leftAxisCtrls =
+        children.where((c) => c.type == "axis" && c.name == "l" && c.isVisible);
+    var topAxisCtrls =
+        children.where((c) => c.type == "axis" && c.name == "t" && c.isVisible);
+    var rightAxisCtrls =
+        children.where((c) => c.type == "axis" && c.name == "r" && c.isVisible);
+    var bottomAxisCtrls =
+        children.where((c) => c.type == "axis" && c.name == "b" && c.isVisible);
+    return BarChartViewModel(
+        control: control,
+        leftAxis: leftAxisCtrls.isNotEmpty
+            ? ChartAxisViewModel.fromStore(store, leftAxisCtrls.first)
+            : null,
+        topAxis: topAxisCtrls.isNotEmpty
+            ? ChartAxisViewModel.fromStore(store, topAxisCtrls.first)
+            : null,
+        rightAxis: rightAxisCtrls.isNotEmpty
+            ? ChartAxisViewModel.fromStore(store, rightAxisCtrls.first)
+            : null,
+        bottomAxis: bottomAxisCtrls.isNotEmpty
+            ? ChartAxisViewModel.fromStore(store, bottomAxisCtrls.first)
+            : null,
+        barGroups: children
+            .where((c) => c.type == "group" && c.isVisible)
+            .map((c) => BarChartGroupViewModel.fromStore(store, c))
+            .toList());
+  }
+
+  @override
+  List<Object?> get props =>
+      [control, leftAxis, rightAxis, topAxis, bottomAxis, barGroups];
+}
 
 class BarChartControl extends StatefulWidget {
   final Control? parent;
@@ -39,7 +169,8 @@ class BarChartControl extends StatefulWidget {
   State<BarChartControl> createState() => _BarChartControlState();
 }
 
-class _BarChartControlState extends State<BarChartControl> {
+class _BarChartControlState extends State<BarChartControl>
+    with FletControlStatefulMixin {
   BarChartEventData? _eventData;
 
   @override
@@ -149,10 +280,8 @@ class _BarChartControlState extends State<BarChartControl> {
                           _eventData = eventData;
                           debugPrint(
                               "BarChart ${widget.control.id} ${eventData.eventType}");
-                          FletAppServices.of(context).server.sendPageEvent(
-                              eventTarget: widget.control.id,
-                              eventName: "chart_event",
-                              eventData: json.encode(eventData));
+                          sendControlEvent(widget.control.id, "chart_event",
+                              json.encode(eventData));
                         }
                       }
                     : null,
diff --git a/package/lib/src/controls/bottom_app_bar.dart b/package/lib/src/controls/bottom_app_bar.dart
index 1fb333db5..13f91a161 100644
--- a/package/lib/src/controls/bottom_app_bar.dart
+++ b/package/lib/src/controls/bottom_app_bar.dart
@@ -1,13 +1,12 @@
-import 'package:flet/src/controls/error.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/controls_view_model.dart';
 import '../utils/colors.dart';
 import '../utils/edge_insets.dart';
 import 'create_control.dart';
+import 'error.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
 
 class BottomAppBarControl extends StatefulWidget {
   final Control? parent;
@@ -26,7 +25,8 @@ class BottomAppBarControl extends StatefulWidget {
   State<BottomAppBarControl> createState() => _BottomAppBarControlState();
 }
 
-class _BottomAppBarControlState extends State<BottomAppBarControl> {
+class _BottomAppBarControlState extends State<BottomAppBarControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   @override
   Widget build(BuildContext context) {
     debugPrint("BottomAppBarControl build: ${widget.control.id}");
@@ -54,32 +54,28 @@ class _BottomAppBarControlState extends State<BottomAppBarControl> {
             e.name.toLowerCase() ==
             widget.control.attrString("clipBehavior", "")!.toLowerCase(),
         orElse: () => Clip.none);
-    var bottomAppBar = StoreConnector<AppState, ControlsViewModel>(
-        distinct: true,
-        converter: (store) => ControlsViewModel.fromStore(
-            store,
-            widget.children
-                .where((c) => c.isVisible && c.name == null)
-                .map((c) => c.id)),
-        builder: (content, viewModel) {
-          return BottomAppBar(
-            clipBehavior: clipBehavior,
-            padding: parseEdgeInsets(widget.control, "padding"),
-            height: widget.control.attrDouble("height"),
-            elevation: elevation,
-            shape: shape,
-            shadowColor: HexColor.fromString(Theme.of(context),
-                widget.control.attrString("shadowColor", "")!),
-            surfaceTintColor: HexColor.fromString(Theme.of(context),
-                widget.control.attrString("surfaceTintColor", "")!),
-            color: HexColor.fromString(
-                Theme.of(context), widget.control.attrString("bgColor", "")!),
-            notchMargin: widget.control.attrDouble("notchMargin", 4.0)!,
-            child: contentCtrls.isNotEmpty
-                ? createControl(widget.control, contentCtrls.first.id, disabled)
-                : null,
-          );
-        });
+    var bottomAppBar = withControls(
+        widget.children
+            .where((c) => c.isVisible && c.name == null)
+            .map((c) => c.id), (content, viewModel) {
+      return BottomAppBar(
+        clipBehavior: clipBehavior,
+        padding: parseEdgeInsets(widget.control, "padding"),
+        height: widget.control.attrDouble("height"),
+        elevation: elevation,
+        shape: shape,
+        shadowColor: HexColor.fromString(
+            Theme.of(context), widget.control.attrString("shadowColor", "")!),
+        surfaceTintColor: HexColor.fromString(Theme.of(context),
+            widget.control.attrString("surfaceTintColor", "")!),
+        color: HexColor.fromString(
+            Theme.of(context), widget.control.attrString("bgColor", "")!),
+        notchMargin: widget.control.attrDouble("notchMargin", 4.0)!,
+        child: contentCtrls.isNotEmpty
+            ? createControl(widget.control, contentCtrls.first.id, disabled)
+            : null,
+      );
+    });
 
     return constrainedControl(
         context, bottomAppBar, widget.parent, widget.control);
diff --git a/package/lib/src/controls/bottom_sheet.dart b/package/lib/src/controls/bottom_sheet.dart
index b615c28dd..e79883db2 100644
--- a/package/lib/src/controls/bottom_sheet.dart
+++ b/package/lib/src/controls/bottom_sheet.dart
@@ -1,19 +1,16 @@
 import 'package:flutter/material.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/colors.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class BottomSheetControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final List<Control> children;
   final bool parentDisabled;
-  final dynamic dispatch;
   final Widget? nextChild;
 
   const BottomSheetControl(
@@ -22,20 +19,18 @@ class BottomSheetControl extends StatefulWidget {
       required this.control,
       required this.children,
       required this.parentDisabled,
-      required this.dispatch,
       required this.nextChild});
 
   @override
   State<BottomSheetControl> createState() => _BottomSheetControlState();
 }
 
-class _BottomSheetControlState extends State<BottomSheetControl> {
+class _BottomSheetControlState extends State<BottomSheetControl>
+    with FletControlStatefulMixin {
   @override
   Widget build(BuildContext context) {
     debugPrint("BottomSheet build: ${widget.control.id}");
 
-    var server = FletAppServices.of(context).server;
-
     bool lastOpen = widget.control.state["open"] ?? false;
     bool disabled = widget.control.isDisabled || widget.parentDisabled;
 
@@ -51,12 +46,7 @@ class _BottomSheetControlState extends State<BottomSheetControl> {
         widget.control.attrBool("maintainBottomViewInsetsPadding", true)!;
 
     void resetOpenState() {
-      List<Map<String, String>> props = [
-        {"i": widget.control.id, "open": "false"}
-      ];
-      widget.dispatch(
-          UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-      server.updateControlProps(props: props);
+      updateControlProps(widget.control.id, {"open": "false"});
     }
 
     if (open && !lastOpen) {
@@ -110,10 +100,7 @@ class _BottomSheetControlState extends State<BottomSheetControl> {
 
           if (shouldDismiss) {
             resetOpenState();
-            server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "dismiss",
-                eventData: "");
+            sendControlEvent(widget.control.id, "dismiss", "");
           }
         });
       });
diff --git a/package/lib/src/controls/canvas.dart b/package/lib/src/controls/canvas.dart
index 7c2cb03c2..1ac733710 100644
--- a/package/lib/src/controls/canvas.dart
+++ b/package/lib/src/controls/canvas.dart
@@ -2,12 +2,12 @@ import 'dart:convert';
 import 'dart:ui' as ui;
 
 import 'package:collection/collection.dart';
+import 'package:equatable/equatable.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_redux/flutter_redux.dart';
+import 'package:redux/redux.dart';
 
-import '../flet_app_services.dart';
 import '../models/app_state.dart';
-import '../models/canvas_view_model.dart';
 import '../models/control.dart';
 import '../models/control_tree_view_model.dart';
 import '../utils/alignment.dart';
@@ -19,6 +19,34 @@ import '../utils/numbers.dart';
 import '../utils/text.dart';
 import '../utils/transforms.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
+
+class CanvasViewModel extends Equatable {
+  final Control control;
+  final Control? child;
+  final List<ControlTreeViewModel> shapes;
+
+  const CanvasViewModel(
+      {required this.control, required this.child, required this.shapes});
+
+  static CanvasViewModel fromStore(
+      Store<AppState> store, Control control, List<Control> children) {
+    return CanvasViewModel(
+        control: control,
+        child: store.state.controls[control.id]!.childIds
+            .map((childId) => store.state.controls[childId])
+            .whereNotNull()
+            .where((c) => c.name == "content" && c.isVisible)
+            .firstOrNull,
+        shapes: children
+            .where((c) => c.name != "content" && c.isVisible)
+            .map((c) => ControlTreeViewModel.fromStore(store, c))
+            .toList());
+  }
+
+  @override
+  List<Object?> get props => [control, shapes];
+}
 
 typedef CanvasControlOnPaintCallback = void Function(Size size);
 
@@ -39,7 +67,8 @@ class CanvasControl extends StatefulWidget {
   State<CanvasControl> createState() => _CanvasControlState();
 }
 
-class _CanvasControlState extends State<CanvasControl> {
+class _CanvasControlState extends State<CanvasControl>
+    with FletControlStatefulMixin {
   int _lastResize = DateTime.now().millisecondsSinceEpoch;
   Size? _lastSize;
 
@@ -71,11 +100,8 @@ class _CanvasControlState extends State<CanvasControl> {
                       _lastSize == null) {
                     _lastResize = now;
                     _lastSize = size;
-                    FletAppServices.of(context).server.sendPageEvent(
-                        eventTarget: viewModel.control.id,
-                        eventName: "resize",
-                        eventData:
-                            json.encode({"w": size.width, "h": size.height}));
+                    sendControlEvent(viewModel.control.id, "resize",
+                        json.encode({"w": size.width, "h": size.height}));
                   }
                 }
               },
diff --git a/package/lib/src/models/chart_axis_view_model.dart b/package/lib/src/controls/charts.dart
similarity index 60%
rename from package/lib/src/models/chart_axis_view_model.dart
rename to package/lib/src/controls/charts.dart
index bfd32655c..9e8c094a2 100644
--- a/package/lib/src/models/chart_axis_view_model.dart
+++ b/package/lib/src/controls/charts.dart
@@ -2,9 +2,29 @@ import 'package:collection/collection.dart';
 import 'package:equatable/equatable.dart';
 import 'package:redux/redux.dart';
 
-import 'app_state.dart';
-import 'chart_axis_label_view_model.dart';
-import 'control.dart';
+import '../models/app_state.dart';
+import '../models/control.dart';
+
+class ChartAxisLabelViewModel extends Equatable {
+  final double value;
+  final Control? control;
+
+  const ChartAxisLabelViewModel({required this.value, required this.control});
+
+  static ChartAxisLabelViewModel fromStore(
+      Store<AppState> store, Control control) {
+    return ChartAxisLabelViewModel(
+        value: control.attrDouble("value")!,
+        control: store.state.controls[control.id]!.childIds
+            .map((childId) => store.state.controls[childId])
+            .whereNotNull()
+            .where((c) => c.isVisible)
+            .firstOrNull);
+  }
+
+  @override
+  List<Object?> get props => [value, control];
+}
 
 class ChartAxisViewModel extends Equatable {
   final Control control;
diff --git a/package/lib/src/controls/checkbox.dart b/package/lib/src/controls/checkbox.dart
index 38827c74c..c9a3134a6 100644
--- a/package/lib/src/controls/checkbox.dart
+++ b/package/lib/src/controls/checkbox.dart
@@ -1,14 +1,11 @@
-import 'package:flet/src/controls/cupertino_checkbox.dart';
-import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
-import '../utils/buttons.dart';
 import '../utils/colors.dart';
 import 'create_control.dart';
+import 'cupertino_checkbox.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
 import 'list_tile.dart';
 
 enum LabelPosition { right, left }
@@ -17,20 +14,19 @@ class CheckboxControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const CheckboxControl(
       {super.key,
       this.parent,
       required this.control,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<CheckboxControl> createState() => _CheckboxControlState();
 }
 
-class _CheckboxControlState extends State<CheckboxControl> {
+class _CheckboxControlState extends State<CheckboxControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   bool? _value;
   bool _tristate = false;
   late final FocusNode _focusNode;
@@ -43,10 +39,8 @@ class _CheckboxControlState extends State<CheckboxControl> {
   }
 
   void _onFocusChange() {
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   @override
@@ -70,92 +64,84 @@ class _CheckboxControlState extends State<CheckboxControl> {
 
   void _onChange(bool? value) {
     var svalue = value != null ? value.toString() : "";
-    setState(() {
-      _value = value;
-    });
-    List<Map<String, String>> props = [
-      {"i": widget.control.id, "value": svalue}
-    ];
-    widget.dispatch(
-        UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-    var server = FletAppServices.of(context).server;
-    server.updateControlProps(props: props);
-    server.sendPageEvent(
-        eventTarget: widget.control.id, eventName: "change", eventData: svalue);
+    _value = value;
+    updateControlProps(widget.control.id, {"value": svalue});
+    sendControlEvent(widget.control.id, "change", svalue);
   }
 
   @override
   Widget build(BuildContext context) {
     debugPrint("Checkbox build: ${widget.control.id}");
-    bool adaptive = widget.control.attrBool("adaptive", false)!;
-    if (adaptive &&
-        (defaultTargetPlatform == TargetPlatform.iOS ||
-            defaultTargetPlatform == TargetPlatform.macOS)) {
-      return CupertinoCheckboxControl(
-          control: widget.control,
-          parentDisabled: widget.parentDisabled,
-          dispatch: widget.dispatch);
-    }
-
-    String label = widget.control.attrString("label", "")!;
-    LabelPosition labelPosition = LabelPosition.values.firstWhere(
-        (p) =>
-            p.name.toLowerCase() ==
-            widget.control.attrString("labelPosition", "")!.toLowerCase(),
-        orElse: () => LabelPosition.right);
-    _tristate = widget.control.attrBool("tristate", false)!;
-    bool autofocus = widget.control.attrBool("autofocus", false)!;
-
-    bool disabled = widget.control.isDisabled || widget.parentDisabled;
-
-    debugPrint("Checkbox StoreConnector build: ${widget.control.id}");
 
-    bool? value = widget.control.attrBool("value", _tristate ? null : false);
-    if (_value != value) {
-      _value = value;
-    }
-
-    var checkbox = Checkbox(
-        autofocus: autofocus,
-        focusNode: _focusNode,
-        value: _value,
-        activeColor: HexColor.fromString(
-            Theme.of(context), widget.control.attrString("activeColor", "")!),
-        focusColor: HexColor.fromString(
-            Theme.of(context), widget.control.attrString("focusColor", "")!),
-        hoverColor: HexColor.fromString(
-            Theme.of(context), widget.control.attrString("hoverColor", "")!),
-        overlayColor: parseMaterialStateColor(
-            Theme.of(context), widget.control, "overlayColor"),
-        checkColor: HexColor.fromString(
-            Theme.of(context), widget.control.attrString("checkColor", "")!),
-        fillColor: parseMaterialStateColor(
-            Theme.of(context), widget.control, "fillColor"),
-        tristate: _tristate,
-        onChanged: !disabled
-            ? (bool? value) {
-                _onChange(value);
-              }
-            : null);
-
-    ListTileClicks.of(context)?.notifier.addListener(() {
-      _toggleValue();
+    return withPagePlatform((context, platform) {
+      bool adaptive = widget.control.attrBool("adaptive", false)!;
+      if (adaptive &&
+          (platform == TargetPlatform.iOS ||
+              platform == TargetPlatform.macOS)) {
+        return CupertinoCheckboxControl(
+            control: widget.control, parentDisabled: widget.parentDisabled);
+      }
+
+      String label = widget.control.attrString("label", "")!;
+      LabelPosition labelPosition = LabelPosition.values.firstWhere(
+          (p) =>
+              p.name.toLowerCase() ==
+              widget.control.attrString("labelPosition", "")!.toLowerCase(),
+          orElse: () => LabelPosition.right);
+      _tristate = widget.control.attrBool("tristate", false)!;
+      bool autofocus = widget.control.attrBool("autofocus", false)!;
+
+      bool disabled = widget.control.isDisabled || widget.parentDisabled;
+
+      debugPrint("Checkbox build: ${widget.control.id}");
+
+      bool? value = widget.control.attrBool("value", _tristate ? null : false);
+      if (_value != value) {
+        _value = value;
+      }
+
+      var checkbox = Checkbox(
+          autofocus: autofocus,
+          focusNode: _focusNode,
+          value: _value,
+          activeColor: HexColor.fromString(
+              Theme.of(context), widget.control.attrString("activeColor", "")!),
+          focusColor: HexColor.fromString(
+              Theme.of(context), widget.control.attrString("focusColor", "")!),
+          hoverColor: HexColor.fromString(
+              Theme.of(context), widget.control.attrString("hoverColor", "")!),
+          overlayColor: parseMaterialStateColor(
+              Theme.of(context), widget.control, "overlayColor"),
+          checkColor: HexColor.fromString(
+              Theme.of(context), widget.control.attrString("checkColor", "")!),
+          fillColor: parseMaterialStateColor(
+              Theme.of(context), widget.control, "fillColor"),
+          tristate: _tristate,
+          onChanged: !disabled
+              ? (bool? value) {
+                  _onChange(value);
+                }
+              : null);
+
+      ListTileClicks.of(context)?.notifier.addListener(() {
+        _toggleValue();
+      });
+
+      Widget result = checkbox;
+      if (label != "") {
+        var labelWidget = disabled
+            ? Text(label,
+                style: TextStyle(color: Theme.of(context).disabledColor))
+            : MouseRegion(cursor: SystemMouseCursors.click, child: Text(label));
+        result = MergeSemantics(
+            child: GestureDetector(
+                onTap: !disabled ? _toggleValue : null,
+                child: labelPosition == LabelPosition.right
+                    ? Row(children: [checkbox, labelWidget])
+                    : Row(children: [labelWidget, checkbox])));
+      }
+
+      return constrainedControl(context, result, widget.parent, widget.control);
     });
-
-    Widget result = checkbox;
-    if (label != "") {
-      var labelWidget = disabled
-          ? Text(label,
-              style: TextStyle(color: Theme.of(context).disabledColor))
-          : MouseRegion(cursor: SystemMouseCursors.click, child: Text(label));
-      result = MergeSemantics(
-          child: GestureDetector(
-              onTap: !disabled ? _toggleValue : null,
-              child: labelPosition == LabelPosition.right
-                  ? Row(children: [checkbox, labelWidget])
-                  : Row(children: [labelWidget, checkbox])));
-    }
-
-    return constrainedControl(context, result, widget.parent, widget.control);
   }
 }
diff --git a/package/lib/src/controls/chip.dart b/package/lib/src/controls/chip.dart
index 1d1b3ab3c..c3c5caccc 100644
--- a/package/lib/src/controls/chip.dart
+++ b/package/lib/src/controls/chip.dart
@@ -1,36 +1,33 @@
 import 'package:flutter/material.dart';
 
 import '../models/control.dart';
+import '../utils/borders.dart';
 import '../utils/colors.dart';
-import 'create_control.dart';
-import 'error.dart';
-import 'package:flet/src/flet_app_services.dart';
-import '../actions.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/edge_insets.dart';
 import '../utils/text.dart';
-import '../utils/borders.dart';
+import 'create_control.dart';
+import 'error.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class ChipControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final List<Control> children;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const ChipControl(
       {super.key,
       this.parent,
       required this.control,
       required this.children,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<ChipControl> createState() => _ChipControlState();
 }
 
-class _ChipControlState extends State<ChipControl> {
+class _ChipControlState extends State<ChipControl>
+    with FletControlStatefulMixin {
   bool _selected = false;
 
   late final FocusNode _focusNode;
@@ -52,27 +49,14 @@ class _ChipControlState extends State<ChipControl> {
   void _onSelect(bool selected) {
     var strSelected = selected.toString();
     debugPrint(strSelected);
-    setState(() {
-      _selected = selected;
-    });
-    List<Map<String, String>> props = [
-      {"i": widget.control.id, "selected": strSelected}
-    ];
-    widget.dispatch(
-        UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-    final server = FletAppServices.of(context).server;
-    server.updateControlProps(props: props);
-    server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: "select",
-        eventData: strSelected);
+    _selected = selected;
+    updateControlProps(widget.control.id, {"selected": strSelected});
+    sendControlEvent(widget.control.id, "select", strSelected);
   }
 
   void _onFocusChange() {
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   @override
@@ -99,7 +83,6 @@ class _ChipControlState extends State<ChipControl> {
     var disabledColor = HexColor.fromString(
         Theme.of(context), widget.control.attrString("disabledColor", "")!);
 
-    final server = FletAppServices.of(context).server;
     bool onClick = widget.control.attrBool("onclick", false)!;
     bool onDelete = widget.control.attrBool("onDelete", false)!;
     bool onSelect = widget.control.attrBool("onSelect", false)!;
@@ -123,20 +106,14 @@ class _ChipControlState extends State<ChipControl> {
     Function()? onClickHandler = onClick && !disabled
         ? () {
             debugPrint("Chip ${widget.control.id} clicked!");
-            server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "click",
-                eventData: "");
+            sendControlEvent(widget.control.id, "click", "");
           }
         : null;
 
     Function()? onDeleteHandler = onDelete && !disabled
         ? () {
             debugPrint("Chip ${widget.control.id} deleted!");
-            server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "delete",
-                eventData: "");
+            sendControlEvent(widget.control.id, "delete", "");
           }
         : null;
 
diff --git a/package/lib/src/controls/clipboard.dart b/package/lib/src/controls/clipboard.dart
index 2ed6b821d..961b74ae3 100644
--- a/package/lib/src/controls/clipboard.dart
+++ b/package/lib/src/controls/clipboard.dart
@@ -1,9 +1,8 @@
-import 'package:flet/src/flet_server.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
 
-import '../flet_app_services.dart';
 import '../models/control.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class ClipboardControl extends StatefulWidget {
   final Control? parent;
@@ -20,12 +19,11 @@ class ClipboardControl extends StatefulWidget {
   State<ClipboardControl> createState() => _ClipboardControlState();
 }
 
-class _ClipboardControlState extends State<ClipboardControl> {
-  FletServer? _server;
-
+class _ClipboardControlState extends State<ClipboardControl>
+    with FletControlStatefulMixin {
   @override
   void deactivate() {
-    _server?.controlInvokeMethods.remove(widget.control.id);
+    unsubscribeMethods(widget.control.id);
     super.deactivate();
   }
 
@@ -33,9 +31,7 @@ class _ClipboardControlState extends State<ClipboardControl> {
   Widget build(BuildContext context) {
     debugPrint("Clipboard build: ${widget.control.id}");
 
-    _server = FletAppServices.of(context).server;
-    _server?.controlInvokeMethods[widget.control.id] =
-        (methodName, args) async {
+    subscribeMethods(widget.control.id, (methodName, args) async {
       switch (methodName) {
         case "set_data":
           await Clipboard.setData(ClipboardData(text: args["data"]!));
@@ -44,7 +40,7 @@ class _ClipboardControlState extends State<ClipboardControl> {
           return (await Clipboard.getData(Clipboard.kTextPlain))?.text;
       }
       return null;
-    };
+    });
 
     return widget.nextChild ?? const SizedBox.shrink();
   }
diff --git a/package/lib/src/controls/column.dart b/package/lib/src/controls/column.dart
index 4b954c6fc..0ae50bc72 100644
--- a/package/lib/src/controls/column.dart
+++ b/package/lib/src/controls/column.dart
@@ -11,15 +11,13 @@ class ColumnControl extends StatelessWidget {
   final Control control;
   final bool parentDisabled;
   final List<Control> children;
-  final dynamic dispatch;
 
   const ColumnControl(
       {super.key,
       this.parent,
       required this.control,
       required this.children,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   Widget build(BuildContext context) {
@@ -73,7 +71,6 @@ class ColumnControl extends StatelessWidget {
     child = ScrollableControl(
       control: control,
       scrollDirection: wrap ? Axis.horizontal : Axis.vertical,
-      dispatch: dispatch,
       child: child,
     );
 
diff --git a/package/lib/src/controls/container.dart b/package/lib/src/controls/container.dart
index e91a326da..9db69816f 100644
--- a/package/lib/src/controls/container.dart
+++ b/package/lib/src/controls/container.dart
@@ -2,15 +2,9 @@ import 'dart:convert';
 import 'dart:typed_data';
 
 import 'package:collection/collection.dart';
-import 'package:flet/src/utils/shadows.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/page_args_model.dart';
-import '../protocol/container_tap_event.dart';
 import '../utils/alignment.dart';
 import '../utils/animations.dart';
 import '../utils/borders.dart';
@@ -19,10 +13,34 @@ import '../utils/edge_insets.dart';
 import '../utils/gradient.dart';
 import '../utils/images.dart';
 import '../utils/launch_url.dart';
+import '../utils/shadows.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateless_mixin.dart';
+import 'flet_store_mixin.dart';
 
-class ContainerControl extends StatelessWidget {
+class ContainerTapEvent {
+  final double localX;
+  final double localY;
+  final double globalX;
+  final double globalY;
+
+  ContainerTapEvent(
+      {required this.localX,
+      required this.localY,
+      required this.globalX,
+      required this.globalY});
+
+  Map<String, dynamic> toJson() => <String, dynamic>{
+        'lx': localX,
+        'ly': localY,
+        'gx': globalX,
+        'gy': globalY
+      };
+}
+
+class ContainerControl extends StatelessWidget
+    with FletControlStatelessMixin, FletStoreMixin {
   final Control? parent;
   final Control control;
   final List<Control> children;
@@ -64,249 +82,228 @@ class ContainerControl extends StatelessWidget {
     var animation = parseAnimation(control, "animate");
     var blur = parseBlur(control, "blur");
 
-    final server = FletAppServices.of(context).server;
-
-    return StoreConnector<AppState, PageArgsModel>(
-        distinct: true,
-        converter: (store) => PageArgsModel.fromStore(store),
-        builder: (context, pageArgs) {
-          DecorationImage? image;
+    return withPageArgs((context, pageArgs) {
+      DecorationImage? image;
 
-          if (imageSrcBase64 != "") {
-            try {
-              Uint8List bytes = base64Decode(imageSrcBase64);
-              image = DecorationImage(
-                  image: MemoryImage(bytes),
-                  repeat: imageRepeat,
-                  fit: imageFit,
-                  opacity: imageOpacity);
-            } catch (ex) {
-              return ErrorControl("Error decoding base64: ${ex.toString()}");
-            }
-          } else if (imageSrc != "") {
-            var assetSrc =
-                getAssetSrc(imageSrc, pageArgs.pageUri!, pageArgs.assetsDir);
+      if (imageSrcBase64 != "") {
+        try {
+          Uint8List bytes = base64Decode(imageSrcBase64);
+          image = DecorationImage(
+              image: MemoryImage(bytes),
+              repeat: imageRepeat,
+              fit: imageFit,
+              opacity: imageOpacity);
+        } catch (ex) {
+          return ErrorControl("Error decoding base64: ${ex.toString()}");
+        }
+      } else if (imageSrc != "") {
+        var assetSrc =
+            getAssetSrc(imageSrc, pageArgs.pageUri!, pageArgs.assetsDir);
 
-            image = DecorationImage(
-                image: assetSrc.isFile
-                    ? getFileImageProvider(assetSrc.path)
-                    : NetworkImage(assetSrc.path),
-                repeat: imageRepeat,
-                fit: imageFit,
-                opacity: imageOpacity);
-          }
+        image = DecorationImage(
+            image: assetSrc.isFile
+                ? getFileImageProvider(assetSrc.path)
+                : NetworkImage(assetSrc.path),
+            repeat: imageRepeat,
+            fit: imageFit,
+            opacity: imageOpacity);
+      }
 
-          var gradient = parseGradient(Theme.of(context), control, "gradient");
-          var blendMode = BlendMode.values.firstWhereOrNull((e) =>
+      var gradient = parseGradient(Theme.of(context), control, "gradient");
+      var blendMode = BlendMode.values.firstWhereOrNull((e) =>
+          e.name.toLowerCase() ==
+          control.attrString("blendMode", "")!.toLowerCase());
+      var shape = BoxShape.values.firstWhere(
+          (e) =>
               e.name.toLowerCase() ==
-              control.attrString("blendMode", "")!.toLowerCase());
-          var shape = BoxShape.values.firstWhere(
-              (e) =>
-                  e.name.toLowerCase() ==
-                  control.attrString("shape", "")!.toLowerCase(),
-              orElse: () => BoxShape.rectangle);
+              control.attrString("shape", "")!.toLowerCase(),
+          orElse: () => BoxShape.rectangle);
 
-          var borderRadius = parseBorderRadius(control, "borderRadius");
+      var borderRadius = parseBorderRadius(control, "borderRadius");
 
-          var clipBehavior = Clip.values.firstWhere(
-              (e) =>
-                  e.name.toLowerCase() ==
-                  control.attrString("clipBehavior", "")!.toLowerCase(),
-              orElse: () => borderRadius != null ? Clip.antiAlias : Clip.none);
-
-          var boxDecor = BoxDecoration(
-              color: bgColor,
-              gradient: gradient,
-              image: image,
-              backgroundBlendMode:
-                  bgColor != null || gradient != null ? blendMode : null,
-              border: parseBorder(Theme.of(context), control, "border"),
-              borderRadius: borderRadius,
-              shape: shape,
-              boxShadow: parseBoxShadow(Theme.of(context), control, "shadow"));
+      var clipBehavior = Clip.values.firstWhere(
+          (e) =>
+              e.name.toLowerCase() ==
+              control.attrString("clipBehavior", "")!.toLowerCase(),
+          orElse: () => borderRadius != null ? Clip.antiAlias : Clip.none);
 
-          Widget? result;
+      var boxDecor = BoxDecoration(
+          color: bgColor,
+          gradient: gradient,
+          image: image,
+          backgroundBlendMode:
+              bgColor != null || gradient != null ? blendMode : null,
+          border: parseBorder(Theme.of(context), control, "border"),
+          borderRadius: borderRadius,
+          shape: shape,
+          boxShadow: parseBoxShadow(Theme.of(context), control, "shadow"));
 
-          if ((onClick || url != "" || onLongPress || onHover) &&
-              ink &&
-              !disabled) {
-            var ink = Ink(
-                decoration: boxDecor,
-                child: InkWell(
-                  // Dummy callback to enable widget
-                  // see https://github.com/flutter/flutter/issues/50116#issuecomment-582047374
-                  // and https://github.com/flutter/flutter/blob/eed80afe2c641fb14b82a22279d2d78c19661787/packages/flutter/lib/src/material/ink_well.dart#L1125-L1129
-                  onTap: onHover ? () {} : null,
-                  onTapDown: onClick || url != ""
-                      ? (details) {
-                          debugPrint("Container ${control.id} clicked!");
-                          if (url != "") {
-                            openWebBrowser(url, webWindowName: urlTarget);
-                          }
-                          if (onClick) {
-                            server.sendPageEvent(
-                                eventTarget: control.id,
-                                eventName: "click",
-                                eventData: json.encode(ContainerTapEvent(
-                                        localX: details.localPosition.dx,
-                                        localY: details.localPosition.dy,
-                                        globalX: details.globalPosition.dx,
-                                        globalY: details.globalPosition.dy)
-                                    .toJson()));
-                          }
-                        }
-                      : null,
-                  onLongPress: onLongPress
-                      ? () {
-                          debugPrint("Container ${control.id} long pressed!");
-                          server.sendPageEvent(
-                              eventTarget: control.id,
-                              eventName: "long_press",
-                              eventData: "");
-                        }
-                      : null,
-                  onHover: onHover
-                      ? (value) {
-                          debugPrint("Container ${control.id} hovered!");
-                          server.sendPageEvent(
-                              eventTarget: control.id,
-                              eventName: "hover",
-                              eventData: value.toString());
-                        }
-                      : null,
-                  borderRadius: borderRadius,
-                  child: Container(
-                    padding: parseEdgeInsets(control, "padding"),
-                    alignment: parseAlignment(control, "alignment"),
-                    clipBehavior: Clip.none,
-                    child: child,
-                  ),
-                ));
+      Widget? result;
 
-            result = animation == null
-                ? Container(
-                    width: control.attrDouble("width"),
-                    height: control.attrDouble("height"),
-                    margin: parseEdgeInsets(control, "margin"),
-                    clipBehavior: Clip.none,
-                    child: ink,
-                  )
-                : AnimatedContainer(
-                    duration: animation.duration,
-                    curve: animation.curve,
-                    width: control.attrDouble("width"),
-                    height: control.attrDouble("height"),
-                    margin: parseEdgeInsets(control, "margin"),
-                    clipBehavior: clipBehavior,
-                    onEnd: control.attrBool("onAnimationEnd", false)!
-                        ? () {
-                            server.sendPageEvent(
-                                eventTarget: control.id,
-                                eventName: "animation_end",
-                                eventData: "container");
-                          }
-                        : null,
-                    child: ink);
-          } else {
-            result = animation == null
-                ? Container(
-                    width: control.attrDouble("width"),
-                    height: control.attrDouble("height"),
-                    padding: parseEdgeInsets(control, "padding"),
-                    margin: parseEdgeInsets(control, "margin"),
-                    alignment: parseAlignment(control, "alignment"),
-                    decoration: boxDecor,
-                    clipBehavior: clipBehavior,
-                    child: child)
-                : AnimatedContainer(
-                    duration: animation.duration,
-                    curve: animation.curve,
-                    width: control.attrDouble("width"),
-                    height: control.attrDouble("height"),
-                    padding: parseEdgeInsets(control, "padding"),
-                    margin: parseEdgeInsets(control, "margin"),
-                    alignment: parseAlignment(control, "alignment"),
-                    decoration: boxDecor,
-                    clipBehavior: clipBehavior,
-                    onEnd: control.attrBool("onAnimationEnd", false)!
-                        ? () {
-                            server.sendPageEvent(
-                                eventTarget: control.id,
-                                eventName: "animation_end",
-                                eventData: "container");
-                          }
-                        : null,
-                    child: child);
+      if ((onClick || url != "" || onLongPress || onHover) &&
+          ink &&
+          !disabled) {
+        var ink = Ink(
+            decoration: boxDecor,
+            child: InkWell(
+              // Dummy callback to enable widget
+              // see https://github.com/flutter/flutter/issues/50116#issuecomment-582047374
+              // and https://github.com/flutter/flutter/blob/eed80afe2c641fb14b82a22279d2d78c19661787/packages/flutter/lib/src/material/ink_well.dart#L1125-L1129
+              onTap: onHover ? () {} : null,
+              onTapDown: onClick || url != ""
+                  ? (details) {
+                      debugPrint("Container ${control.id} clicked!");
+                      if (url != "") {
+                        openWebBrowser(url, webWindowName: urlTarget);
+                      }
+                      if (onClick) {
+                        sendControlEvent(
+                            context,
+                            control.id,
+                            "click",
+                            json.encode(ContainerTapEvent(
+                                    localX: details.localPosition.dx,
+                                    localY: details.localPosition.dy,
+                                    globalX: details.globalPosition.dx,
+                                    globalY: details.globalPosition.dy)
+                                .toJson()));
+                      }
+                    }
+                  : null,
+              onLongPress: onLongPress
+                  ? () {
+                      debugPrint("Container ${control.id} long pressed!");
+                      sendControlEvent(context, control.id, "long_press", "");
+                    }
+                  : null,
+              onHover: onHover
+                  ? (value) {
+                      debugPrint("Container ${control.id} hovered!");
+                      sendControlEvent(
+                          context, control.id, "hover", value.toString());
+                    }
+                  : null,
+              borderRadius: borderRadius,
+              child: Container(
+                padding: parseEdgeInsets(control, "padding"),
+                alignment: parseAlignment(control, "alignment"),
+                clipBehavior: Clip.none,
+                child: child,
+              ),
+            ));
 
-            if ((onClick || onLongPress || onHover || url != "") && !disabled) {
-              result = MouseRegion(
-                cursor: onClick || url != ""
-                    ? SystemMouseCursors.click
-                    : MouseCursor.defer,
-                onEnter: onHover
-                    ? (value) {
-                        debugPrint(
-                            "Container's mouse region ${control.id} entered!");
-                        server.sendPageEvent(
-                            eventTarget: control.id,
-                            eventName: "hover",
-                            eventData: "true");
+        result = animation == null
+            ? Container(
+                width: control.attrDouble("width"),
+                height: control.attrDouble("height"),
+                margin: parseEdgeInsets(control, "margin"),
+                clipBehavior: Clip.none,
+                child: ink,
+              )
+            : AnimatedContainer(
+                duration: animation.duration,
+                curve: animation.curve,
+                width: control.attrDouble("width"),
+                height: control.attrDouble("height"),
+                margin: parseEdgeInsets(control, "margin"),
+                clipBehavior: clipBehavior,
+                onEnd: control.attrBool("onAnimationEnd", false)!
+                    ? () {
+                        sendControlEvent(
+                            context, control.id, "animation_end", "container");
                       }
                     : null,
-                onExit: onHover
-                    ? (value) {
-                        debugPrint(
-                            "Container's mouse region ${control.id} exited!");
-                        server.sendPageEvent(
-                            eventTarget: control.id,
-                            eventName: "hover",
-                            eventData: "false");
+                child: ink);
+      } else {
+        result = animation == null
+            ? Container(
+                width: control.attrDouble("width"),
+                height: control.attrDouble("height"),
+                padding: parseEdgeInsets(control, "padding"),
+                margin: parseEdgeInsets(control, "margin"),
+                alignment: parseAlignment(control, "alignment"),
+                decoration: boxDecor,
+                clipBehavior: clipBehavior,
+                child: child)
+            : AnimatedContainer(
+                duration: animation.duration,
+                curve: animation.curve,
+                width: control.attrDouble("width"),
+                height: control.attrDouble("height"),
+                padding: parseEdgeInsets(control, "padding"),
+                margin: parseEdgeInsets(control, "margin"),
+                alignment: parseAlignment(control, "alignment"),
+                decoration: boxDecor,
+                clipBehavior: clipBehavior,
+                onEnd: control.attrBool("onAnimationEnd", false)!
+                    ? () {
+                        sendControlEvent(
+                            context, control.id, "animation_end", "container");
                       }
                     : null,
-                child: GestureDetector(
-                  onTapDown: onClick || url != ""
-                      ? (details) {
-                          debugPrint("Container ${control.id} clicked!");
-                          if (url != "") {
-                            openWebBrowser(url, webWindowName: urlTarget);
-                          }
-                          if (onClick) {
-                            server.sendPageEvent(
-                                eventTarget: control.id,
-                                eventName: "click",
-                                eventData: json.encode(ContainerTapEvent(
-                                        localX: details.localPosition.dx,
-                                        localY: details.localPosition.dy,
-                                        globalX: details.globalPosition.dx,
-                                        globalY: details.globalPosition.dy)
-                                    .toJson()));
-                          }
-                        }
-                      : null,
-                  onLongPress: onLongPress
-                      ? () {
-                          debugPrint("Container ${control.id} clicked!");
-                          server.sendPageEvent(
-                              eventTarget: control.id,
-                              eventName: "long_press",
-                              eventData: "");
-                        }
-                      : null,
-                  child: result,
-                ),
-              );
-            }
-          }
+                child: child);
+
+        if ((onClick || onLongPress || onHover || url != "") && !disabled) {
+          result = MouseRegion(
+            cursor: onClick || url != ""
+                ? SystemMouseCursors.click
+                : MouseCursor.defer,
+            onEnter: onHover
+                ? (value) {
+                    debugPrint(
+                        "Container's mouse region ${control.id} entered!");
+                    sendControlEvent(context, control.id, "hover", "true");
+                  }
+                : null,
+            onExit: onHover
+                ? (value) {
+                    debugPrint(
+                        "Container's mouse region ${control.id} exited!");
+                    sendControlEvent(context, control.id, "hover", "false");
+                  }
+                : null,
+            child: GestureDetector(
+              onTapDown: onClick || url != ""
+                  ? (details) {
+                      debugPrint("Container ${control.id} clicked!");
+                      if (url != "") {
+                        openWebBrowser(url, webWindowName: urlTarget);
+                      }
+                      if (onClick) {
+                        sendControlEvent(
+                            context,
+                            control.id,
+                            "click",
+                            json.encode(ContainerTapEvent(
+                                    localX: details.localPosition.dx,
+                                    localY: details.localPosition.dy,
+                                    globalX: details.globalPosition.dx,
+                                    globalY: details.globalPosition.dy)
+                                .toJson()));
+                      }
+                    }
+                  : null,
+              onLongPress: onLongPress
+                  ? () {
+                      debugPrint("Container ${control.id} clicked!");
+                      sendControlEvent(context, control.id, "long_press", "");
+                    }
+                  : null,
+              child: result,
+            ),
+          );
+        }
+      }
 
-          if (blur != null) {
-            result = borderRadius != null
-                ? ClipRRect(
-                    borderRadius: borderRadius,
-                    child: BackdropFilter(filter: blur, child: result))
-                : ClipRect(child: BackdropFilter(filter: blur, child: result));
-          }
+      if (blur != null) {
+        result = borderRadius != null
+            ? ClipRRect(
+                borderRadius: borderRadius,
+                child: BackdropFilter(filter: blur, child: result))
+            : ClipRect(child: BackdropFilter(filter: blur, child: result));
+      }
 
-          return constrainedControl(context, result, parent, control);
-        });
+      return constrainedControl(context, result, parent, control);
+    });
   }
 }
diff --git a/package/lib/src/controls/create_control.dart b/package/lib/src/controls/create_control.dart
index 0d6dc9bbb..ed67b6342 100644
--- a/package/lib/src/controls/create_control.dart
+++ b/package/lib/src/controls/create_control.dart
@@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_redux/flutter_redux.dart';
 
+import '../control_factory.dart';
 import '../flet_app_services.dart';
 import '../flet_server.dart';
 import '../models/app_state.dart';
@@ -133,8 +134,19 @@ Widget createControl(Control? parent, String id, bool parentDisabled,
         }
       }
 
-      // create control widget
-      var widget = createWidget(controlKey, controlView, parent, parentDisabled,
+      Widget? widget;
+
+      for (var createControlFactory
+          in FletAppServices.of(context).createControlFactories) {
+        widget = createControlFactory(CreateControlArgs(controlKey, parent,
+            controlView.control, controlView.children, parentDisabled));
+        if (widget != null) {
+          break;
+        }
+      }
+
+      // try creating Flet built-in widget
+      widget ??= createWidget(controlKey, controlView, parent, parentDisabled,
           nextChild, FletAppServices.of(context).server);
 
       // no theme defined? return widget!
@@ -153,7 +165,7 @@ Widget createControl(Control? parent, String id, bool parentDisabled,
         return Theme(
             data: parseTheme(controlView.control, "theme", brightness,
                 parentTheme: parentTheme),
-            child: widget);
+            child: widget!);
       }
 
       if (themeMode == ThemeMode.system) {
@@ -210,10 +222,7 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent,
           parentDisabled: parentDisabled);
     case "audio":
       return AudioControl(
-          parent: parent,
-          control: controlView.control,
-          dispatch: controlView.dispatch,
-          nextChild: nextChild);
+          parent: parent, control: controlView.control, nextChild: nextChild);
     case "divider":
       return DividerControl(
           key: key, parent: parent, control: controlView.control);
@@ -258,16 +267,14 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent,
           parent: parent,
           control: controlView.control,
           children: controlView.children,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "pagelet":
       return PageletControl(
           key: key,
           parent: parent,
           control: controlView.control,
           children: controlView.children,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "progressring":
       return ProgressRingControl(
           key: key, parent: parent, control: controlView.control);
@@ -325,21 +332,18 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent,
           parentDisabled: parentDisabled);
     case "column":
       return ColumnControl(
-        key: key,
-        parent: parent,
-        control: controlView.control,
-        children: controlView.children,
-        parentDisabled: parentDisabled,
-        dispatch: controlView.dispatch,
-      );
+          key: key,
+          parent: parent,
+          control: controlView.control,
+          children: controlView.children,
+          parentDisabled: parentDisabled);
     case "row":
       return RowControl(
           key: key,
           parent: parent,
           control: controlView.control,
           children: controlView.children,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "responsiverow":
       return ResponsiveRowControl(
           key: key,
@@ -374,16 +378,14 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent,
           parent: parent,
           control: controlView.control,
           children: controlView.children,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "expansionpanellist":
       return ExpansionPanelListControl(
           key: key,
           parent: parent,
           control: controlView.control,
           children: controlView.children,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "stack":
       return StackControl(
           key: key,
@@ -404,7 +406,6 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent,
         control: controlView.control,
         children: controlView.children,
         parentDisabled: parentDisabled,
-        dispatch: controlView.dispatch,
       );
     case "timepicker":
       return TimePickerControl(
@@ -412,7 +413,6 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent,
         control: controlView.control,
         children: controlView.children,
         parentDisabled: parentDisabled,
-        dispatch: controlView.dispatch,
       );
     case "draggable":
       return DraggableControl(
@@ -511,17 +511,14 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent,
           parent: parent,
           control: controlView.control,
           children: controlView.children,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "gridview":
       return GridViewControl(
-        key: key,
-        parent: parent,
-        control: controlView.control,
-        children: controlView.children,
-        parentDisabled: parentDisabled,
-        dispatch: controlView.dispatch,
-      );
+          key: key,
+          parent: parent,
+          control: controlView.control,
+          children: controlView.children,
+          parentDisabled: parentDisabled);
     case "textfield":
       return TextFieldControl(
           key: key,
@@ -542,60 +539,49 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent,
           parent: parent,
           control: controlView.control,
           children: controlView.children,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "checkbox":
       return CheckboxControl(
           key: key,
           parent: parent,
           control: controlView.control,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "cupertinocheckbox":
       return CupertinoCheckboxControl(
           key: key,
           parent: parent,
           control: controlView.control,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "switch":
       return SwitchControl(
           key: key,
           parent: parent,
           control: controlView.control,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "cupertinoswitch":
       return CupertinoSwitchControl(
           key: key,
           parent: parent,
           control: controlView.control,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "slider":
       return SliderControl(
-        key: key,
-        parent: parent,
-        control: controlView.control,
-        parentDisabled: parentDisabled,
-        dispatch: controlView.dispatch,
-      );
+          key: key,
+          parent: parent,
+          control: controlView.control,
+          parentDisabled: parentDisabled);
     case "cupertinoslider":
       return CupertinoSliderControl(
-        key: key,
-        parent: parent,
-        control: controlView.control,
-        parentDisabled: parentDisabled,
-        dispatch: controlView.dispatch,
-      );
+          key: key,
+          parent: parent,
+          control: controlView.control,
+          parentDisabled: parentDisabled);
     case "rangeslider":
       return RangeSliderControl(
-        key: key,
-        parent: parent,
-        control: controlView.control,
-        parentDisabled: parentDisabled,
-        dispatch: controlView.dispatch,
-      );
+          key: key,
+          parent: parent,
+          control: controlView.control,
+          parentDisabled: parentDisabled);
     case "radiogroup":
       return RadioGroupControl(
           key: key,
@@ -608,22 +594,19 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent,
           key: key,
           parent: parent,
           control: controlView.control,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "cupertinoradio":
       return CupertinoRadioControl(
           key: key,
           parent: parent,
           control: controlView.control,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "dropdown":
       return DropdownControl(
           key: key,
           parent: parent,
           control: controlView.control,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "snackbar":
       return SnackBarControl(
           parent: parent,
@@ -636,8 +619,7 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent,
           parent: parent,
           control: controlView.control,
           children: controlView.children,
-          parentDisabled: parentDisabled,
-          server: server);
+          parentDisabled: parentDisabled);
     case "alertdialog":
       return AlertDialogControl(
           parent: parent,
@@ -658,7 +640,6 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent,
           control: controlView.control,
           children: controlView.children,
           parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch,
           nextChild: nextChild);
     case "banner":
       return BannerControl(
@@ -673,30 +654,26 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent,
           parent: parent,
           control: controlView.control,
           children: controlView.children,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "navigationrail":
       return NavigationRailControl(
           key: key,
           parent: parent,
           control: controlView.control,
           children: controlView.children,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "navigationbar":
       return NavigationBarControl(
           parent: parent,
           control: controlView.control,
           children: controlView.children,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "cupertinonavigationbar":
       return CupertinoNavigationBarControl(
           parent: parent,
           control: controlView.control,
           children: controlView.children,
-          parentDisabled: parentDisabled,
-          dispatch: controlView.dispatch);
+          parentDisabled: parentDisabled);
     case "bottomappbar":
       return BottomAppBarControl(
         parent: parent,
diff --git a/package/lib/src/controls/cupertino_alert_dialog.dart b/package/lib/src/controls/cupertino_alert_dialog.dart
index 91d09c8f9..8dee779a8 100644
--- a/package/lib/src/controls/cupertino_alert_dialog.dart
+++ b/package/lib/src/controls/cupertino_alert_dialog.dart
@@ -1,14 +1,10 @@
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class CupertinoAlertDialogControl extends StatefulWidget {
   final Control? parent;
@@ -31,7 +27,7 @@ class CupertinoAlertDialogControl extends StatefulWidget {
 }
 
 class _CupertinoAlertDialogControlState
-    extends State<CupertinoAlertDialogControl> {
+    extends State<CupertinoAlertDialogControl> with FletControlStatefulMixin {
   Widget _createCupertinoAlertDialog() {
     bool disabled = widget.control.isDisabled || widget.parentDisabled;
     var titleCtrls =
@@ -62,66 +58,50 @@ class _CupertinoAlertDialogControlState
   Widget build(BuildContext context) {
     debugPrint("CupertinoAlertDialog build ($hashCode): ${widget.control.id}");
 
-    var server = FletAppServices.of(context).server;
-
     bool lastOpen = widget.control.state["open"] ?? false;
 
-    return StoreConnector<AppState, Function>(
-        distinct: true,
-        converter: (store) => store.dispatch,
-        builder: (context, dispatch) {
-          debugPrint(
-              "CupertinoAlertDialog StoreConnector build: ${widget.control.id}");
-
-          var open = widget.control.attrBool("open", false)!;
-          var modal = widget.control.attrBool("modal", false)!;
-
-          debugPrint("Current open state: $lastOpen");
-          debugPrint("New open state: $open");
-
-          if (open && (open != lastOpen)) {
-            var dialog = _createCupertinoAlertDialog();
-            if (dialog is ErrorControl) {
-              return dialog;
-            }
-
-            // close previous dialog
-            if (ModalRoute.of(context)?.isCurrent != true) {
-              Navigator.of(context).pop();
-            }
-
-            widget.control.state["open"] = open;
-
-            WidgetsBinding.instance.addPostFrameCallback((_) {
-              showDialog(
-                  barrierDismissible: !modal,
-                  useRootNavigator: false,
-                  context: context,
-                  builder: (context) => dialog).then((value) {
-                lastOpen = widget.control.state["open"] ?? false;
-                debugPrint("Dialog should be dismissed ($hashCode): $lastOpen");
-                bool shouldDismiss = lastOpen;
-                widget.control.state["open"] = false;
-
-                if (shouldDismiss) {
-                  List<Map<String, String>> props = [
-                    {"i": widget.control.id, "open": "false"}
-                  ];
-                  dispatch(UpdateControlPropsAction(
-                      UpdateControlPropsPayload(props: props)));
-                  server.updateControlProps(props: props);
-                  server.sendPageEvent(
-                      eventTarget: widget.control.id,
-                      eventName: "dismiss",
-                      eventData: "");
-                }
-              });
-            });
-          } else if (open != lastOpen && lastOpen) {
-            Navigator.of(context).pop();
+    debugPrint("CupertinoAlertDialog build: ${widget.control.id}");
+
+    var open = widget.control.attrBool("open", false)!;
+    var modal = widget.control.attrBool("modal", false)!;
+
+    debugPrint("Current open state: $lastOpen");
+    debugPrint("New open state: $open");
+
+    if (open && (open != lastOpen)) {
+      var dialog = _createCupertinoAlertDialog();
+      if (dialog is ErrorControl) {
+        return dialog;
+      }
+
+      // close previous dialog
+      if (ModalRoute.of(context)?.isCurrent != true) {
+        Navigator.of(context).pop();
+      }
+
+      widget.control.state["open"] = open;
+
+      WidgetsBinding.instance.addPostFrameCallback((_) {
+        showDialog(
+            barrierDismissible: !modal,
+            useRootNavigator: false,
+            context: context,
+            builder: (context) => dialog).then((value) {
+          lastOpen = widget.control.state["open"] ?? false;
+          debugPrint("Dialog should be dismissed ($hashCode): $lastOpen");
+          bool shouldDismiss = lastOpen;
+          widget.control.state["open"] = false;
+
+          if (shouldDismiss) {
+            updateControlProps(widget.control.id, {"open": "false"});
+            sendControlEvent(widget.control.id, "dismiss", "");
           }
-
-          return widget.nextChild ?? const SizedBox.shrink();
         });
+      });
+    } else if (open != lastOpen && lastOpen) {
+      Navigator.of(context).pop();
+    }
+
+    return widget.nextChild ?? const SizedBox.shrink();
   }
 }
diff --git a/package/lib/src/controls/cupertino_checkbox.dart b/package/lib/src/controls/cupertino_checkbox.dart
index b669b73bd..822fa4760 100644
--- a/package/lib/src/controls/cupertino_checkbox.dart
+++ b/package/lib/src/controls/cupertino_checkbox.dart
@@ -1,12 +1,10 @@
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/colors.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
 import 'list_tile.dart';
 
 enum LabelPosition { right, left }
@@ -15,20 +13,19 @@ class CupertinoCheckboxControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const CupertinoCheckboxControl(
       {super.key,
       this.parent,
       required this.control,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<CupertinoCheckboxControl> createState() => _CheckboxControlState();
 }
 
-class _CheckboxControlState extends State<CupertinoCheckboxControl> {
+class _CheckboxControlState extends State<CupertinoCheckboxControl>
+    with FletControlStatefulMixin {
   bool? _value;
   bool _tristate = false;
   late final FocusNode _focusNode;
@@ -41,10 +38,8 @@ class _CheckboxControlState extends State<CupertinoCheckboxControl> {
   }
 
   void _onFocusChange() {
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   @override
@@ -68,18 +63,9 @@ class _CheckboxControlState extends State<CupertinoCheckboxControl> {
 
   void _onChange(bool? value) {
     var svalue = value != null ? value.toString() : "";
-    setState(() {
-      _value = value;
-    });
-    List<Map<String, String>> props = [
-      {"i": widget.control.id, "value": svalue}
-    ];
-    widget.dispatch(
-        UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-    var server = FletAppServices.of(context).server;
-    server.updateControlProps(props: props);
-    server.sendPageEvent(
-        eventTarget: widget.control.id, eventName: "change", eventData: svalue);
+    _value = value;
+    updateControlProps(widget.control.id, {"value": svalue});
+    sendControlEvent(widget.control.id, "change", svalue);
   }
 
   @override
@@ -96,7 +82,7 @@ class _CheckboxControlState extends State<CupertinoCheckboxControl> {
     bool autofocus = widget.control.attrBool("autofocus", false)!;
     bool disabled = widget.control.isDisabled || widget.parentDisabled;
 
-    debugPrint("CupertinoCheckbox StoreConnector build: ${widget.control.id}");
+    debugPrint("CupertinoCheckbox build: ${widget.control.id}");
 
     bool? value = widget.control.attrBool("value", _tristate ? null : false);
     if (_value != value) {
diff --git a/package/lib/src/controls/cupertino_dialog_action.dart b/package/lib/src/controls/cupertino_dialog_action.dart
index 4c14ac05a..bc08b2813 100644
--- a/package/lib/src/controls/cupertino_dialog_action.dart
+++ b/package/lib/src/controls/cupertino_dialog_action.dart
@@ -1,31 +1,29 @@
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 
-import '../flet_app_services.dart';
 import '../models/control.dart';
 import '../utils/text.dart';
 import 'create_control.dart';
+import 'flet_control_stateless_mixin.dart';
 
-class CupertinoDialogActionControl extends StatelessWidget {
+class CupertinoDialogActionControl extends StatelessWidget
+    with FletControlStatelessMixin {
   final Control? parent;
   final Control control;
   final List<Control> children;
   final bool parentDisabled;
 
   const CupertinoDialogActionControl(
-      {Key? key,
+      {super.key,
       this.parent,
       required this.control,
       required this.children,
-      required this.parentDisabled})
-      : super(key: key);
+      required this.parentDisabled});
 
   @override
   Widget build(BuildContext context) {
     debugPrint("CupertinoDialogAction build: ${control.id}");
 
-    final server = FletAppServices.of(context).server;
-
     String text = control.attrString("text", "")!;
     var contentCtrls = children.where((c) => c.name == "content");
     bool isDefaultAction = control.attrBool("isDefaultAction", false)!;
@@ -35,8 +33,7 @@ class CupertinoDialogActionControl extends StatelessWidget {
     Function()? onPressed = !disabled
         ? () {
             debugPrint("CupertinoDialogAction ${control.id} clicked!");
-            server.sendPageEvent(
-                eventTarget: control.id, eventName: "click", eventData: "");
+            sendControlEvent(context, control.id, "click", "");
           }
         : null;
 
diff --git a/package/lib/src/controls/cupertino_navigation_bar.dart b/package/lib/src/controls/cupertino_navigation_bar.dart
index 2dfa2bb9b..e57fc7f49 100644
--- a/package/lib/src/controls/cupertino_navigation_bar.dart
+++ b/package/lib/src/controls/cupertino_navigation_bar.dart
@@ -1,32 +1,26 @@
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/controls_view_model.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/borders.dart';
 import '../utils/colors.dart';
 import '../utils/icons.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
 
 class CupertinoNavigationBarControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final List<Control> children;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const CupertinoNavigationBarControl(
       {super.key,
       this.parent,
       required this.control,
       required this.children,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<CupertinoNavigationBarControl> createState() =>
@@ -34,23 +28,16 @@ class CupertinoNavigationBarControl extends StatefulWidget {
 }
 
 class _CupertinoNavigationBarControlState
-    extends State<CupertinoNavigationBarControl> {
+    extends State<CupertinoNavigationBarControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   int _selectedIndex = 0;
 
   void _onTap(int index) {
     _selectedIndex = index;
     debugPrint("Selected index: $_selectedIndex");
-    List<Map<String, String>> props = [
-      {"i": widget.control.id, "selectedindex": _selectedIndex.toString()}
-    ];
-    widget.dispatch(
-        UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-    final server = FletAppServices.of(context).server;
-    server.updateControlProps(props: props);
-    server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: "change",
-        eventData: _selectedIndex.toString());
+    updateControlProps(
+        widget.control.id, {"selectedindex": _selectedIndex.toString()});
+    sendControlEvent(widget.control.id, "change", _selectedIndex.toString());
   }
 
   @override
@@ -64,53 +51,49 @@ class _CupertinoNavigationBarControlState
       _selectedIndex = selectedIndex;
     }
 
-    var navBar = StoreConnector<AppState, ControlsViewModel>(
-        distinct: true,
-        converter: (store) => ControlsViewModel.fromStore(
-            store,
-            widget.children
-                .where((c) => c.isVisible && c.name == null)
-                .map((c) => c.id)),
-        builder: (content, viewModel) {
-          return CupertinoTabBar(
-              backgroundColor: HexColor.fromString(
-                  Theme.of(context), widget.control.attrString("bgColor", "")!),
-              activeColor: HexColor.fromString(
-                  Theme.of(context), widget.control.attrString("activeColor", "")!),
-              inactiveColor: HexColor.fromString(Theme.of(context),
-                  widget.control.attrString("inactiveColor", "")!) ?? CupertinoColors.inactiveGray,
-              iconSize: widget.control.attrDouble("iconSize", 30.0)!,
-              currentIndex: _selectedIndex,
-              border: parseBorder(Theme.of(context), widget.control, "border"),
-              onTap: _onTap,
-              items: viewModel.controlViews.map((destView) {
-                var label = destView.control.attrString("label", "")!;
+    var navBar = withControls(
+        widget.children
+            .where((c) => c.isVisible && c.name == null)
+            .map((c) => c.id), (content, viewModel) {
+      return CupertinoTabBar(
+          backgroundColor: HexColor.fromString(
+              Theme.of(context), widget.control.attrString("bgColor", "")!),
+          activeColor: HexColor.fromString(
+              Theme.of(context), widget.control.attrString("activeColor", "")!),
+          inactiveColor: HexColor.fromString(Theme.of(context),
+                  widget.control.attrString("inactiveColor", "")!) ??
+              CupertinoColors.inactiveGray,
+          iconSize: widget.control.attrDouble("iconSize", 30.0)!,
+          currentIndex: _selectedIndex,
+          border: parseBorder(Theme.of(context), widget.control, "border"),
+          onTap: _onTap,
+          items: viewModel.controlViews.map((destView) {
+            var label = destView.control.attrString("label", "")!;
 
-                var icon =
-                    parseIcon(destView.control.attrString("icon", "")!);
-                var iconContentCtrls =
-                    destView.children.where((c) => c.name == "icon_content");
+            var icon = parseIcon(destView.control.attrString("icon", "")!);
+            var iconContentCtrls =
+                destView.children.where((c) => c.name == "icon_content");
 
-                var selectedIcon = parseIcon(
-                    destView.control.attrString("selectedIcon", "")!);
-                var selectedIconContentCtrls = destView.children
-                    .where((c) => c.name == "selected_icon_content");
+            var selectedIcon =
+                parseIcon(destView.control.attrString("selectedIcon", "")!);
+            var selectedIconContentCtrls = destView.children
+                .where((c) => c.name == "selected_icon_content");
 
-                return BottomNavigationBarItem(
-                    tooltip: destView.control.attrString("tooltip", "")!,
-                    icon: iconContentCtrls.isNotEmpty
-                        ? createControl(destView.control,
-                            iconContentCtrls.first.id, disabled)
-                        : Icon(icon),
-                    activeIcon: selectedIconContentCtrls.isNotEmpty
-                        ? createControl(destView.control,
-                            selectedIconContentCtrls.first.id, disabled)
-                        : selectedIcon != null
-                            ? Icon(selectedIcon)
-                            : null,
-                    label: label);
-              }).toList());
-        });
+            return BottomNavigationBarItem(
+                tooltip: destView.control.attrString("tooltip", "")!,
+                icon: iconContentCtrls.isNotEmpty
+                    ? createControl(
+                        destView.control, iconContentCtrls.first.id, disabled)
+                    : Icon(icon),
+                activeIcon: selectedIconContentCtrls.isNotEmpty
+                    ? createControl(destView.control,
+                        selectedIconContentCtrls.first.id, disabled)
+                    : selectedIcon != null
+                        ? Icon(selectedIcon)
+                        : null,
+                label: label);
+          }).toList());
+    });
 
     return constrainedControl(context, navBar, widget.parent, widget.control);
   }
diff --git a/package/lib/src/controls/cupertino_radio.dart b/package/lib/src/controls/cupertino_radio.dart
index f36eeff36..5e75104c0 100644
--- a/package/lib/src/controls/cupertino_radio.dart
+++ b/package/lib/src/controls/cupertino_radio.dart
@@ -1,16 +1,12 @@
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/control_ancestor_view_model.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/colors.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
 import 'list_tile.dart';
 
 enum LabelPosition { right, left }
@@ -19,20 +15,19 @@ class CupertinoRadioControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const CupertinoRadioControl(
       {super.key,
       this.parent,
       required this.control,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<CupertinoRadioControl> createState() => _CupertinoRadioControlState();
 }
 
-class _CupertinoRadioControlState extends State<CupertinoRadioControl> {
+class _CupertinoRadioControlState extends State<CupertinoRadioControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   late final FocusNode _focusNode;
 
   @override
@@ -43,10 +38,8 @@ class _CupertinoRadioControlState extends State<CupertinoRadioControl> {
   }
 
   void _onFocusChange() {
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   @override
@@ -59,16 +52,8 @@ class _CupertinoRadioControlState extends State<CupertinoRadioControl> {
   void _onChange(String ancestorId, String? value) {
     var svalue = value ?? "";
     debugPrint(svalue);
-    List<Map<String, String>> props = [
-      {"i": ancestorId, "value": svalue}
-    ];
-    widget.dispatch(
-        UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-
-    final server = FletAppServices.of(context).server;
-    server.updateControlProps(props: props);
-    server.sendPageEvent(
-        eventTarget: ancestorId, eventName: "change", eventData: svalue);
+    updateControlProps(ancestorId, {"value": svalue});
+    sendControlEvent(ancestorId, "change", svalue);
   }
 
   @override
@@ -85,69 +70,60 @@ class _CupertinoRadioControlState extends State<CupertinoRadioControl> {
     bool autofocus = widget.control.attrBool("autofocus", false)!;
     bool disabled = widget.control.isDisabled || widget.parentDisabled;
 
-    return StoreConnector<AppState, ControlAncestorViewModel>(
-        distinct: true,
-        ignoreChange: (state) {
-          return state.controls[widget.control.id] == null;
-        },
-        converter: (store) => ControlAncestorViewModel.fromStore(
-            store, widget.control.id, "radiogroup"),
-        builder: (context, viewModel) {
-          debugPrint(
-              "CupertinoRadio StoreConnector build: ${widget.control.id}");
-
-          if (viewModel.ancestor == null) {
-            return const ErrorControl(
-                "CupertinoRadio control must be enclosed with RadioGroup.");
-          }
-
-          String groupValue = viewModel.ancestor!.attrString("value", "")!;
-          String ancestorId = viewModel.ancestor!.id;
-
-          var cupertinoRadio = CupertinoRadio<String>(
-              autofocus: autofocus,
-              focusNode: _focusNode,
-              groupValue: groupValue,
-              value: value,
-              useCheckmarkStyle:
-                  widget.control.attrBool("useCheckmarkStyle", false)!,
-              fillColor: HexColor.fromString(Theme.of(context),
-                  widget.control.attrString("fillColor", "")!),
-              activeColor: HexColor.fromString(Theme.of(context),
-                  widget.control.attrString("activeColor", "")!),
-              inactiveColor: HexColor.fromString(Theme.of(context),
-                  widget.control.attrString("inactiveColor", "")!),
-              onChanged: !disabled
-                  ? (String? value) {
-                      _onChange(ancestorId, value);
-                    }
-                  : null);
-
-          ListTileClicks.of(context)?.notifier.addListener(() {
-            _onChange(ancestorId, value);
-          });
-
-          Widget result = cupertinoRadio;
-          if (label != "") {
-            var labelWidget = disabled
-                ? Text(label,
-                    style: TextStyle(color: Theme.of(context).disabledColor))
-                : MouseRegion(
-                    cursor: SystemMouseCursors.click, child: Text(label));
-            result = MergeSemantics(
-                child: GestureDetector(
-                    onTap: !disabled
-                        ? () {
-                            _onChange(ancestorId, value);
-                          }
-                        : null,
-                    child: labelPosition == LabelPosition.right
-                        ? Row(children: [cupertinoRadio, labelWidget])
-                        : Row(children: [labelWidget, cupertinoRadio])));
-          }
-
-          return constrainedControl(
-              context, result, widget.parent, widget.control);
-        });
+    return withControlAncestor(widget.control.id, "radiogroup",
+        (context, viewModel) {
+      debugPrint("CupertinoRadio build: ${widget.control.id}");
+
+      if (viewModel.ancestor == null) {
+        return const ErrorControl(
+            "CupertinoRadio control must be enclosed with RadioGroup.");
+      }
+
+      String groupValue = viewModel.ancestor!.attrString("value", "")!;
+      String ancestorId = viewModel.ancestor!.id;
+
+      var cupertinoRadio = CupertinoRadio<String>(
+          autofocus: autofocus,
+          focusNode: _focusNode,
+          groupValue: groupValue,
+          value: value,
+          useCheckmarkStyle:
+              widget.control.attrBool("useCheckmarkStyle", false)!,
+          fillColor: HexColor.fromString(
+              Theme.of(context), widget.control.attrString("fillColor", "")!),
+          activeColor: HexColor.fromString(
+              Theme.of(context), widget.control.attrString("activeColor", "")!),
+          inactiveColor: HexColor.fromString(Theme.of(context),
+              widget.control.attrString("inactiveColor", "")!),
+          onChanged: !disabled
+              ? (String? value) {
+                  _onChange(ancestorId, value);
+                }
+              : null);
+
+      ListTileClicks.of(context)?.notifier.addListener(() {
+        _onChange(ancestorId, value);
+      });
+
+      Widget result = cupertinoRadio;
+      if (label != "") {
+        var labelWidget = disabled
+            ? Text(label,
+                style: TextStyle(color: Theme.of(context).disabledColor))
+            : MouseRegion(cursor: SystemMouseCursors.click, child: Text(label));
+        result = MergeSemantics(
+            child: GestureDetector(
+                onTap: !disabled
+                    ? () {
+                        _onChange(ancestorId, value);
+                      }
+                    : null,
+                child: labelPosition == LabelPosition.right
+                    ? Row(children: [cupertinoRadio, labelWidget])
+                    : Row(children: [labelWidget, cupertinoRadio])));
+      }
+
+      return constrainedControl(context, result, widget.parent, widget.control);
+    });
   }
 }
diff --git a/package/lib/src/controls/cupertino_slider.dart b/package/lib/src/controls/cupertino_slider.dart
index a88bb445e..464e50ee1 100644
--- a/package/lib/src/controls/cupertino_slider.dart
+++ b/package/lib/src/controls/cupertino_slider.dart
@@ -1,32 +1,30 @@
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
-import '../actions.dart';
-import '../flet_app_services.dart';
+
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/colors.dart';
-import '../utils/desktop.dart';
 import '../utils/debouncer.dart';
+import '../utils/desktop.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class CupertinoSliderControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const CupertinoSliderControl(
       {super.key,
       this.parent,
       required this.control,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<CupertinoSliderControl> createState() => _CupertinoSliderControlState();
 }
 
-class _CupertinoSliderControlState extends State<CupertinoSliderControl> {
+class _CupertinoSliderControlState extends State<CupertinoSliderControl>
+    with FletControlStatefulMixin {
   double _value = 0;
   final _debouncer = Debouncer(milliseconds: isDesktop() ? 10 : 100);
 
@@ -39,21 +37,12 @@ class _CupertinoSliderControlState extends State<CupertinoSliderControl> {
   void onChange(double value) {
     var svalue = value.toString();
     debugPrint(svalue);
-    setState(() {
-      _value = value;
-    });
-
-    List<Map<String, String>> props = [
-      {"i": widget.control.id, "value": svalue}
-    ];
-    widget.dispatch(
-        UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-
+    _value = value;
+    var props = {"value": svalue};
+    updateControlProps(widget.control.id, props, clientOnly: true);
     _debouncer.run(() {
-      final server = FletAppServices.of(context).server;
-      server.updateControlProps(props: props);
-      server.sendPageEvent(
-          eventTarget: widget.control.id, eventName: "change", eventData: '');
+      updateControlProps(widget.control.id, props);
+      sendControlEvent(widget.control.id, "change", '');
     });
   }
 
@@ -67,10 +56,7 @@ class _CupertinoSliderControlState extends State<CupertinoSliderControl> {
     double max = widget.control.attrDouble("max", 1)!;
     int? divisions = widget.control.attrInt("divisions");
 
-    final server = FletAppServices.of(context).server;
-
-    debugPrint(
-        "CupertinoSliderControl StoreConnector build: ${widget.control.id}");
+    debugPrint("CupertinoSliderControl build: ${widget.control.id}");
 
     double value = widget.control.attrDouble("value", 0)!;
     if (_value != value) {
@@ -101,18 +87,14 @@ class _CupertinoSliderControlState extends State<CupertinoSliderControl> {
             : null,
         onChangeStart: !disabled
             ? (double value) {
-                server.sendPageEvent(
-                    eventTarget: widget.control.id,
-                    eventName: "change_start",
-                    eventData: value.toString());
+                sendControlEvent(
+                    widget.control.id, "change_start", value.toString());
               }
             : null,
         onChangeEnd: !disabled
             ? (double value) {
-                server.sendPageEvent(
-                    eventTarget: widget.control.id,
-                    eventName: "change_end",
-                    eventData: value.toString());
+                sendControlEvent(
+                    widget.control.id, "change_end", value.toString());
               }
             : null);
 
diff --git a/package/lib/src/controls/cupertino_switch.dart b/package/lib/src/controls/cupertino_switch.dart
index cccf9c6c0..704494bd5 100644
--- a/package/lib/src/controls/cupertino_switch.dart
+++ b/package/lib/src/controls/cupertino_switch.dart
@@ -1,15 +1,10 @@
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
-import '../utils/buttons.dart';
 import '../utils/colors.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
 import 'list_tile.dart';
 
 enum LabelPosition { right, left }
@@ -18,20 +13,19 @@ class CupertinoSwitchControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const CupertinoSwitchControl(
       {super.key,
       this.parent,
       required this.control,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<CupertinoSwitchControl> createState() => _CupertinoSwitchControlState();
 }
 
-class _CupertinoSwitchControlState extends State<CupertinoSwitchControl> {
+class _CupertinoSwitchControlState extends State<CupertinoSwitchControl>
+    with FletControlStatefulMixin {
   bool _value = false;
   late final FocusNode _focusNode;
 
@@ -52,25 +46,14 @@ class _CupertinoSwitchControlState extends State<CupertinoSwitchControl> {
   void _onChange(bool value) {
     var svalue = value.toString();
     debugPrint(svalue);
-    setState(() {
-      _value = value;
-    });
-    List<Map<String, String>> props = [
-      {"i": widget.control.id, "value": svalue}
-    ];
-    widget.dispatch(
-        UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-    final server = FletAppServices.of(context).server;
-    server.updateControlProps(props: props);
-    server.sendPageEvent(
-        eventTarget: widget.control.id, eventName: "change", eventData: svalue);
+    _value = value;
+    updateControlProps(widget.control.id, {"value": svalue});
+    sendControlEvent(widget.control.id, "change", svalue);
   }
 
   void _onFocusChange() {
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   @override
@@ -86,65 +69,57 @@ class _CupertinoSwitchControlState extends State<CupertinoSwitchControl> {
     bool autofocus = widget.control.attrBool("autofocus", false)!;
     bool disabled = widget.control.isDisabled || widget.parentDisabled;
 
-    return StoreConnector<AppState, Function>(
-        distinct: true,
-        converter: (store) => store.dispatch,
-        builder: (context, dispatch) {
-          debugPrint(
-              "CupertinoSwitch StoreConnector build: ${widget.control.id}");
-
-          bool value = widget.control.attrBool("value", false)!;
-          if (_value != value) {
-            _value = value;
-          }
-
-          var materialThumbColor = parseMaterialStateColor(
-              Theme.of(context), widget.control, "thumbColor");
-
-          var materialTrackColor = parseMaterialStateColor(
-              Theme.of(context), widget.control, "trackColor");
-
-          var swtch = CupertinoSwitch(
-              autofocus: autofocus,
-              focusNode: _focusNode,
-              activeColor: HexColor.fromString(Theme.of(context),
-                  widget.control.attrString("activeColor", "")!),
-              thumbColor: materialThumbColor?.resolve({}),
-              trackColor: materialTrackColor?.resolve({}),
-              focusColor: HexColor.fromString(Theme.of(context),
-                  widget.control.attrString("focusColor", "")!),
-              value: _value,
-              onChanged: !disabled
-                  ? (bool value) {
-                      _onChange(value);
+    debugPrint("CupertinoSwitch build: ${widget.control.id}");
+
+    bool value = widget.control.attrBool("value", false)!;
+    if (_value != value) {
+      _value = value;
+    }
+
+    var materialThumbColor = parseMaterialStateColor(
+        Theme.of(context), widget.control, "thumbColor");
+
+    var materialTrackColor = parseMaterialStateColor(
+        Theme.of(context), widget.control, "trackColor");
+
+    var swtch = CupertinoSwitch(
+        autofocus: autofocus,
+        focusNode: _focusNode,
+        activeColor: HexColor.fromString(
+            Theme.of(context), widget.control.attrString("activeColor", "")!),
+        thumbColor: materialThumbColor?.resolve({}),
+        trackColor: materialTrackColor?.resolve({}),
+        focusColor: HexColor.fromString(
+            Theme.of(context), widget.control.attrString("focusColor", "")!),
+        value: _value,
+        onChanged: !disabled
+            ? (bool value) {
+                _onChange(value);
+              }
+            : null);
+
+    ListTileClicks.of(context)?.notifier.addListener(() {
+      _onChange(!_value);
+    });
+
+    Widget result = swtch;
+    if (label != "") {
+      var labelWidget = disabled
+          ? Text(label,
+              style: TextStyle(color: Theme.of(context).disabledColor))
+          : MouseRegion(cursor: SystemMouseCursors.click, child: Text(label));
+      result = MergeSemantics(
+          child: GestureDetector(
+              onTap: !disabled
+                  ? () {
+                      _onChange(!_value);
                     }
-                  : null);
-
-          ListTileClicks.of(context)?.notifier.addListener(() {
-            _onChange(!_value);
-          });
-
-          Widget result = swtch;
-          if (label != "") {
-            var labelWidget = disabled
-                ? Text(label,
-                    style: TextStyle(color: Theme.of(context).disabledColor))
-                : MouseRegion(
-                    cursor: SystemMouseCursors.click, child: Text(label));
-            result = MergeSemantics(
-                child: GestureDetector(
-                    onTap: !disabled
-                        ? () {
-                            _onChange(!_value);
-                          }
-                        : null,
-                    child: labelPosition == LabelPosition.right
-                        ? Row(children: [swtch, labelWidget])
-                        : Row(children: [labelWidget, swtch])));
-          }
-
-          return constrainedControl(
-              context, result, widget.parent, widget.control);
-        });
+                  : null,
+              child: labelPosition == LabelPosition.right
+                  ? Row(children: [swtch, labelWidget])
+                  : Row(children: [labelWidget, swtch])));
+    }
+
+    return constrainedControl(context, result, widget.parent, widget.control);
   }
 }
diff --git a/package/lib/src/controls/cupertino_textfield.dart b/package/lib/src/controls/cupertino_textfield.dart
index d84e86a78..34954c8af 100644
--- a/package/lib/src/controls/cupertino_textfield.dart
+++ b/package/lib/src/controls/cupertino_textfield.dart
@@ -1,15 +1,9 @@
 import 'package:collection/collection.dart';
-import 'package:flet/src/controls/textfield.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/borders.dart';
 import '../utils/colors.dart';
 import '../utils/gradient.dart';
@@ -17,7 +11,9 @@ import '../utils/shadows.dart';
 import '../utils/text.dart';
 import '../utils/textfield.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
 import 'form_field.dart';
+import 'textfield.dart';
 
 class CupertinoTextFieldControl extends StatefulWidget {
   final Control? parent;
@@ -37,9 +33,10 @@ class CupertinoTextFieldControl extends StatefulWidget {
       _CupertinoTextFieldControlState();
 }
 
-class _CupertinoTextFieldControlState extends State<CupertinoTextFieldControl> {
+class _CupertinoTextFieldControlState extends State<CupertinoTextFieldControl>
+    with FletControlStatefulMixin {
   String _value = "";
-  bool _revealPassword = false;
+  final bool _revealPassword = false;
   bool _focused = false;
   late TextEditingController _controller;
   late final FocusNode _focusNode;
@@ -54,10 +51,7 @@ class _CupertinoTextFieldControlState extends State<CupertinoTextFieldControl> {
       onKey: (FocusNode node, RawKeyEvent evt) {
         if (!evt.isShiftPressed && evt.logicalKey.keyLabel == 'Enter') {
           if (evt is RawKeyDownEvent) {
-            FletAppServices.of(context).server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "submit",
-                eventData: "");
+            sendControlEvent(widget.control.id, "submit", "");
           }
           return KeyEventResult.handled;
         } else {
@@ -84,20 +78,16 @@ class _CupertinoTextFieldControlState extends State<CupertinoTextFieldControl> {
     setState(() {
       _focused = _shiftEnterfocusNode.hasFocus;
     });
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _shiftEnterfocusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(widget.control.id,
+        _shiftEnterfocusNode.hasFocus ? "focus" : "blur", "");
   }
 
   void _onFocusChange() {
     setState(() {
       _focused = _focusNode.hasFocus;
     });
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   @override
@@ -107,229 +97,193 @@ class _CupertinoTextFieldControlState extends State<CupertinoTextFieldControl> {
     bool autofocus = widget.control.attrBool("autofocus", false)!;
     bool disabled = widget.control.isDisabled || widget.parentDisabled;
 
-    return StoreConnector<AppState, Function>(
-        distinct: true,
-        converter: (store) => store.dispatch,
-        builder: (context, dispatch) {
-          debugPrint(
-              "CupertinoTextField StoreConnector build: ${widget.control.id}");
-
-          String value = widget.control.attrs["value"] ?? "";
-          if (_value != value) {
-            _value = value;
-            _controller.text = value;
-          }
-
-          var prefixControls =
-              widget.children.where((c) => c.name == "prefix" && c.isVisible);
-          var suffixControls =
-              widget.children.where((c) => c.name == "suffix" && c.isVisible);
-
-          bool shiftEnter = widget.control.attrBool("shiftEnter", false)!;
-          bool multiline =
-              widget.control.attrBool("multiline", false)! || shiftEnter;
-          int minLines = widget.control.attrInt("minLines", 1)!;
-          int? maxLines =
-              widget.control.attrInt("maxLines", multiline ? null : 1);
-
-          bool readOnly = widget.control.attrBool("readOnly", false)!;
-          bool password = widget.control.attrBool("password", false)!;
-          bool onChange = widget.control.attrBool("onChange", false)!;
-
-          var cursorColor = HexColor.fromString(
-              Theme.of(context), widget.control.attrString("cursorColor", "")!);
-          var selectionColor = HexColor.fromString(Theme.of(context),
-              widget.control.attrString("selectionColor", "")!);
-
-          int? maxLength = widget.control.attrInt("maxLength");
-
-          var textSize = widget.control.attrDouble("textSize");
-
-          var color = HexColor.fromString(
-              Theme.of(context), widget.control.attrString("color", "")!);
-          var focusedColor = HexColor.fromString(Theme.of(context),
-              widget.control.attrString("focusedColor", "")!);
-
-          TextStyle? textStyle =
-              parseTextStyle(Theme.of(context), widget.control, "textStyle");
-          if (textSize != null || color != null || focusedColor != null) {
-            textStyle = (textStyle ?? const TextStyle()).copyWith(
-                fontSize: textSize,
-                color: _focused ? focusedColor ?? color : color);
-          }
-
-          TextCapitalization? textCapitalization = TextCapitalization.values
-              .firstWhere(
-                  (a) =>
-                      a.name.toLowerCase() ==
-                      widget.control
-                          .attrString("capitalization", "")!
-                          .toLowerCase(),
-                  orElse: () => TextCapitalization.none);
-
-          FilteringTextInputFormatter? inputFilter =
-              parseInputFilter(widget.control, "inputFilter");
-
-          List<TextInputFormatter>? inputFormatters = [];
-          // add non-null input formatters
-          if (inputFilter != null) {
-            inputFormatters.add(inputFilter);
-          }
-          if (textCapitalization != TextCapitalization.none) {
-            inputFormatters
-                .add(TextCapitalizationFormatter(textCapitalization));
-          }
-
-          TextInputType keyboardType = parseTextInputType(
-              widget.control.attrString("keyboardType", "")!);
-
-          if (multiline) {
-            keyboardType = TextInputType.multiline;
-          }
-
-          TextAlign textAlign = TextAlign.values.firstWhere(
-            ((b) =>
-                b.name ==
-                widget.control.attrString("textAlign", "")!.toLowerCase()),
-            orElse: () => TextAlign.start,
-          );
-
-          bool autocorrect = widget.control.attrBool("autocorrect", true)!;
-          bool enableSuggestions =
-              widget.control.attrBool("enableSuggestions", true)!;
-          bool smartDashesType =
-              widget.control.attrBool("smartDashesType", true)!;
-          bool smartQuotesType =
-              widget.control.attrBool("smartQuotesType", true)!;
-
-          FocusNode focusNode = shiftEnter ? _shiftEnterfocusNode : _focusNode;
-
-          var focusValue = widget.control.attrString("focus");
-          if (focusValue != null && focusValue != _lastFocusValue) {
-            _lastFocusValue = focusValue;
-            focusNode.requestFocus();
-          }
+    debugPrint("CupertinoTextField StoreConnector build: ${widget.control.id}");
+
+    String value = widget.control.attrs["value"] ?? "";
+    if (_value != value) {
+      _value = value;
+      _controller.text = value;
+    }
+
+    var prefixControls =
+        widget.children.where((c) => c.name == "prefix" && c.isVisible);
+    var suffixControls =
+        widget.children.where((c) => c.name == "suffix" && c.isVisible);
+
+    bool shiftEnter = widget.control.attrBool("shiftEnter", false)!;
+    bool multiline = widget.control.attrBool("multiline", false)! || shiftEnter;
+    int minLines = widget.control.attrInt("minLines", 1)!;
+    int? maxLines = widget.control.attrInt("maxLines", multiline ? null : 1);
+
+    bool readOnly = widget.control.attrBool("readOnly", false)!;
+    bool password = widget.control.attrBool("password", false)!;
+    bool onChange = widget.control.attrBool("onChange", false)!;
+
+    var cursorColor = HexColor.fromString(
+        Theme.of(context), widget.control.attrString("cursorColor", "")!);
+    var selectionColor = HexColor.fromString(
+        Theme.of(context), widget.control.attrString("selectionColor", "")!);
+
+    int? maxLength = widget.control.attrInt("maxLength");
+
+    var textSize = widget.control.attrDouble("textSize");
+
+    var color = HexColor.fromString(
+        Theme.of(context), widget.control.attrString("color", "")!);
+    var focusedColor = HexColor.fromString(
+        Theme.of(context), widget.control.attrString("focusedColor", "")!);
+
+    TextStyle? textStyle =
+        parseTextStyle(Theme.of(context), widget.control, "textStyle");
+    if (textSize != null || color != null || focusedColor != null) {
+      textStyle = (textStyle ?? const TextStyle()).copyWith(
+          fontSize: textSize, color: _focused ? focusedColor ?? color : color);
+    }
+
+    TextCapitalization? textCapitalization = TextCapitalization.values
+        .firstWhere(
+            (a) =>
+                a.name.toLowerCase() ==
+                widget.control.attrString("capitalization", "")!.toLowerCase(),
+            orElse: () => TextCapitalization.none);
+
+    FilteringTextInputFormatter? inputFilter =
+        parseInputFilter(widget.control, "inputFilter");
+
+    List<TextInputFormatter>? inputFormatters = [];
+    // add non-null input formatters
+    if (inputFilter != null) {
+      inputFormatters.add(inputFilter);
+    }
+    if (textCapitalization != TextCapitalization.none) {
+      inputFormatters.add(TextCapitalizationFormatter(textCapitalization));
+    }
+
+    TextInputType keyboardType =
+        parseTextInputType(widget.control.attrString("keyboardType", "")!);
+
+    if (multiline) {
+      keyboardType = TextInputType.multiline;
+    }
+
+    TextAlign textAlign = TextAlign.values.firstWhere(
+      ((b) =>
+          b.name == widget.control.attrString("textAlign", "")!.toLowerCase()),
+      orElse: () => TextAlign.start,
+    );
 
-          BoxDecoration? defaultDecoration =
-              const CupertinoTextField().decoration;
-          var gradient =
-              parseGradient(Theme.of(context), widget.control, "gradient");
-          var blendMode = BlendMode.values.firstWhereOrNull((e) =>
-              e.name.toLowerCase() ==
-              widget.control.attrString("blendMode", "")!.toLowerCase());
-
-          var borderRadius = parseBorderRadius(widget.control, "borderRadius");
-          var bgColor = HexColor.fromString(
-              Theme.of(context), widget.control.attrString("bgColor", "")!);
-
-          Widget textField = CupertinoTextField(
-              style: textStyle,
-              placeholder: widget.control.attrString("placeholderText"),
-              placeholderStyle: parseTextStyle(
-                  Theme.of(context), widget.control, "placeholderStyle"),
-              autofocus: autofocus,
-              enabled: !disabled,
-              onSubmitted: !multiline
-                  ? (_) {
-                      FletAppServices.of(context).server.sendPageEvent(
-                          eventTarget: widget.control.id,
-                          eventName: "submit",
-                          eventData: "");
-                    }
-                  : null,
-              decoration: defaultDecoration?.copyWith(
-                  color: bgColor,
-                  gradient: gradient,
-                  backgroundBlendMode:
-                      bgColor != null || gradient != null ? blendMode : null,
-                  border:
-                      parseBorder(Theme.of(context), widget.control, "border"),
-                  borderRadius: borderRadius,
-                  boxShadow: parseBoxShadow(
-                      Theme.of(context), widget.control, "shadow")),
-              cursorHeight: widget.control.attrDouble("cursorHeight"),
-              showCursor: widget.control.attrBool("showCursor"),
-              cursorWidth: widget.control.attrDouble("cursorWidth") ?? 2.0,
-              cursorRadius: parseRadius(widget.control, "cursorRadius") ??
-                  const Radius.circular(2.0),
-              keyboardType: keyboardType,
-              autocorrect: autocorrect,
-              enableSuggestions: enableSuggestions,
-              smartDashesType: smartDashesType
-                  ? SmartDashesType.enabled
-                  : SmartDashesType.disabled,
-              smartQuotesType: smartQuotesType
-                  ? SmartQuotesType.enabled
-                  : SmartQuotesType.disabled,
-              suffixMode: parseVisibilityMode(
-                  widget.control.attrString("suffixVisibilityMode", "")!),
-              prefixMode: parseVisibilityMode(
-                  widget.control.attrString("prefixVisibilityMode", "")!),
-              textAlign: textAlign,
-              minLines: minLines,
-              maxLines: maxLines,
-              maxLength: maxLength,
-              prefix: prefixControls.isNotEmpty
-                  ? createControl(
-                      widget.control, prefixControls.first.id, disabled)
-                  : null,
-              suffix: suffixControls.isNotEmpty
-                  ? createControl(
-                      widget.control, suffixControls.first.id, disabled)
-                  : null,
-              readOnly: readOnly,
-              inputFormatters:
-                  inputFormatters.isNotEmpty ? inputFormatters : null,
-              obscureText: password && !_revealPassword,
-              controller: _controller,
-              focusNode: focusNode,
-              onChanged: (String value) {
-                //debugPrint(value);
-                setState(() {
-                  _value = value;
-                });
-                List<Map<String, String>> props = [
-                  {"i": widget.control.id, "value": value}
-                ];
-                dispatch(UpdateControlPropsAction(
-                    UpdateControlPropsPayload(props: props)));
-                FletAppServices.of(context)
-                    .server
-                    .updateControlProps(props: props);
-                if (onChange) {
-                  FletAppServices.of(context).server.sendPageEvent(
-                      eventTarget: widget.control.id,
-                      eventName: "change",
-                      eventData: value);
-                }
-              });
-
-          if (cursorColor != null || selectionColor != null) {
-            textField = TextSelectionTheme(
-                data: TextSelectionTheme.of(context).copyWith(
-                    cursorColor: cursorColor, selectionColor: selectionColor),
-                child: textField);
+    bool autocorrect = widget.control.attrBool("autocorrect", true)!;
+    bool enableSuggestions =
+        widget.control.attrBool("enableSuggestions", true)!;
+    bool smartDashesType = widget.control.attrBool("smartDashesType", true)!;
+    bool smartQuotesType = widget.control.attrBool("smartQuotesType", true)!;
+
+    FocusNode focusNode = shiftEnter ? _shiftEnterfocusNode : _focusNode;
+
+    var focusValue = widget.control.attrString("focus");
+    if (focusValue != null && focusValue != _lastFocusValue) {
+      _lastFocusValue = focusValue;
+      focusNode.requestFocus();
+    }
+
+    BoxDecoration? defaultDecoration = const CupertinoTextField().decoration;
+    var gradient = parseGradient(Theme.of(context), widget.control, "gradient");
+    var blendMode = BlendMode.values.firstWhereOrNull((e) =>
+        e.name.toLowerCase() ==
+        widget.control.attrString("blendMode", "")!.toLowerCase());
+
+    var borderRadius = parseBorderRadius(widget.control, "borderRadius");
+    var bgColor = HexColor.fromString(
+        Theme.of(context), widget.control.attrString("bgColor", "")!);
+
+    Widget textField = CupertinoTextField(
+        style: textStyle,
+        placeholder: widget.control.attrString("placeholderText"),
+        placeholderStyle: parseTextStyle(
+            Theme.of(context), widget.control, "placeholderStyle"),
+        autofocus: autofocus,
+        enabled: !disabled,
+        onSubmitted: !multiline
+            ? (_) {
+                sendControlEvent(widget.control.id, "submit", "");
+              }
+            : null,
+        decoration: defaultDecoration?.copyWith(
+            color: bgColor,
+            gradient: gradient,
+            backgroundBlendMode:
+                bgColor != null || gradient != null ? blendMode : null,
+            border: parseBorder(Theme.of(context), widget.control, "border"),
+            borderRadius: borderRadius,
+            boxShadow:
+                parseBoxShadow(Theme.of(context), widget.control, "shadow")),
+        cursorHeight: widget.control.attrDouble("cursorHeight"),
+        showCursor: widget.control.attrBool("showCursor"),
+        cursorWidth: widget.control.attrDouble("cursorWidth") ?? 2.0,
+        cursorRadius: parseRadius(widget.control, "cursorRadius") ??
+            const Radius.circular(2.0),
+        keyboardType: keyboardType,
+        autocorrect: autocorrect,
+        enableSuggestions: enableSuggestions,
+        smartDashesType: smartDashesType
+            ? SmartDashesType.enabled
+            : SmartDashesType.disabled,
+        smartQuotesType: smartQuotesType
+            ? SmartQuotesType.enabled
+            : SmartQuotesType.disabled,
+        suffixMode: parseVisibilityMode(
+            widget.control.attrString("suffixVisibilityMode", "")!),
+        prefixMode: parseVisibilityMode(
+            widget.control.attrString("prefixVisibilityMode", "")!),
+        textAlign: textAlign,
+        minLines: minLines,
+        maxLines: maxLines,
+        maxLength: maxLength,
+        prefix: prefixControls.isNotEmpty
+            ? createControl(widget.control, prefixControls.first.id, disabled)
+            : null,
+        suffix: suffixControls.isNotEmpty
+            ? createControl(widget.control, suffixControls.first.id, disabled)
+            : null,
+        readOnly: readOnly,
+        inputFormatters: inputFormatters.isNotEmpty ? inputFormatters : null,
+        obscureText: password && !_revealPassword,
+        controller: _controller,
+        focusNode: focusNode,
+        onChanged: (String value) {
+          //debugPrint(value);
+          _value = value;
+          updateControlProps(widget.control.id, {"value": value});
+          if (onChange) {
+            sendControlEvent(widget.control.id, "change", value);
           }
+        });
 
-          if (widget.control.attrInt("expand", 0)! > 0) {
-            return constrainedControl(
-                context, textField, widget.parent, widget.control);
-          } else {
-            return LayoutBuilder(
-              builder: (BuildContext context, BoxConstraints constraints) {
-                if (constraints.maxWidth == double.infinity &&
-                    widget.control.attrDouble("width") == null) {
-                  textField = ConstrainedBox(
-                    constraints: const BoxConstraints.tightFor(width: 300),
-                    child: textField,
-                  );
-                }
-
-                return constrainedControl(
-                    context, textField, widget.parent, widget.control);
-              },
+    if (cursorColor != null || selectionColor != null) {
+      textField = TextSelectionTheme(
+          data: TextSelectionTheme.of(context).copyWith(
+              cursorColor: cursorColor, selectionColor: selectionColor),
+          child: textField);
+    }
+
+    if (widget.control.attrInt("expand", 0)! > 0) {
+      return constrainedControl(
+          context, textField, widget.parent, widget.control);
+    } else {
+      return LayoutBuilder(
+        builder: (BuildContext context, BoxConstraints constraints) {
+          if (constraints.maxWidth == double.infinity &&
+              widget.control.attrDouble("width") == null) {
+            textField = ConstrainedBox(
+              constraints: const BoxConstraints.tightFor(width: 300),
+              child: textField,
             );
           }
-        });
+
+          return constrainedControl(
+              context, textField, widget.parent, widget.control);
+        },
+      );
+    }
   }
 }
diff --git a/package/lib/src/controls/datatable.dart b/package/lib/src/controls/datatable.dart
index c35d69ede..aa5f5da52 100644
--- a/package/lib/src/controls/datatable.dart
+++ b/package/lib/src/controls/datatable.dart
@@ -1,18 +1,15 @@
 import 'dart:convert';
 
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/controls_view_model.dart';
 import '../utils/borders.dart';
-import '../utils/buttons.dart';
 import '../utils/colors.dart';
 import '../utils/gradient.dart';
 import '../utils/text.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
 
 class DataTableControl extends StatefulWidget {
   final Control? parent;
@@ -31,188 +28,157 @@ class DataTableControl extends StatefulWidget {
   State<DataTableControl> createState() => _DataTableControlState();
 }
 
-class _DataTableControlState extends State<DataTableControl> {
+class _DataTableControlState extends State<DataTableControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   @override
   Widget build(BuildContext context) {
     debugPrint("DataTableControl build: ${widget.control.id}");
 
     bool tableDisabled = widget.control.isDisabled || widget.parentDisabled;
 
-    var server = FletAppServices.of(context).server;
+    var datatable =
+        withControls(widget.children.where((c) => c.isVisible).map((c) => c.id),
+            (content, viewModel) {
+      var bgColor = widget.control.attrString("bgColor");
+      var border = parseBorder(Theme.of(context), widget.control, "border");
+      var borderRadius = parseBorderRadius(widget.control, "borderRadius");
+      var gradient =
+          parseGradient(Theme.of(context), widget.control, "gradient");
+      var horizontalLines =
+          parseBorderSide(Theme.of(context), widget.control, "horizontalLines");
+      var verticalLines =
+          parseBorderSide(Theme.of(context), widget.control, "verticalLines");
+      var defaultDecoration =
+          Theme.of(context).dataTableTheme.decoration ?? const BoxDecoration();
 
-    var datatable = StoreConnector<AppState, ControlsViewModel>(
-        distinct: true,
-        converter: (store) => ControlsViewModel.fromStore(
-            store, widget.children.where((c) => c.isVisible).map((c) => c.id)),
-        builder: (content, viewModel) {
-          var bgColor = widget.control.attrString("bgColor");
-          var border = parseBorder(Theme.of(context), widget.control, "border");
-          var borderRadius = parseBorderRadius(widget.control, "borderRadius");
-          var gradient =
-              parseGradient(Theme.of(context), widget.control, "gradient");
-          var horizontalLines = parseBorderSide(
-              Theme.of(context), widget.control, "horizontalLines");
-          var verticalLines = parseBorderSide(
-              Theme.of(context), widget.control, "verticalLines");
-          var defaultDecoration = Theme.of(context).dataTableTheme.decoration ??
-              const BoxDecoration();
+      BoxDecoration? decoration;
+      if (bgColor != null ||
+          border != null ||
+          borderRadius != null ||
+          gradient != null) {
+        decoration = (defaultDecoration as BoxDecoration).copyWith(
+            color: HexColor.fromString(Theme.of(context), bgColor ?? ""),
+            border: border,
+            borderRadius: borderRadius,
+            gradient: gradient);
+      }
 
-          BoxDecoration? decoration;
-          if (bgColor != null ||
-              border != null ||
-              borderRadius != null ||
-              gradient != null) {
-            decoration = (defaultDecoration as BoxDecoration).copyWith(
-                color: HexColor.fromString(Theme.of(context), bgColor ?? ""),
-                border: border,
-                borderRadius: borderRadius,
-                gradient: gradient);
-          }
+      TableBorder? tableBorder;
+      if (horizontalLines != null || verticalLines != null) {
+        tableBorder = TableBorder(
+            horizontalInside: horizontalLines ?? BorderSide.none,
+            verticalInside: verticalLines ?? BorderSide.none);
+      }
 
-          TableBorder? tableBorder;
-          if (horizontalLines != null || verticalLines != null) {
-            tableBorder = TableBorder(
-                horizontalInside: horizontalLines ?? BorderSide.none,
-                verticalInside: verticalLines ?? BorderSide.none);
-          }
-
-          return DataTable(
-              decoration: decoration,
-              border: tableBorder,
-              checkboxHorizontalMargin:
-                  widget.control.attrDouble("checkboxHorizontalMargin"),
-              columnSpacing: widget.control.attrDouble("columnSpacing"),
-              dataRowColor: parseMaterialStateColor(
-                  Theme.of(context), widget.control, "dataRowColor"),
-              dataRowMinHeight: widget.control.attrDouble("dataRowMinHeight"),
-              dataRowMaxHeight: widget.control.attrDouble("dataRowMaxHeight"),
-              dataTextStyle: parseTextStyle(
-                  Theme.of(context), widget.control, "dataTextStyle"),
-              headingRowColor: parseMaterialStateColor(
-                  Theme.of(context), widget.control, "headingRowColor"),
-              headingRowHeight: widget.control.attrDouble("headingRowHeight"),
-              headingTextStyle: parseTextStyle(
-                  Theme.of(context), widget.control, "headingTextStyle"),
-              dividerThickness: widget.control.attrDouble("dividerThickness"),
-              horizontalMargin: widget.control.attrDouble("horizontalMargin"),
-              showBottomBorder:
-                  widget.control.attrBool("showBottomBorder", false)!,
-              showCheckboxColumn:
-                  widget.control.attrBool("showCheckboxColumn", false)!,
-              sortAscending: widget.control.attrBool("sortAscending", false)!,
-              sortColumnIndex: widget.control.attrInt("sortColumnIndex"),
-              onSelectAll: widget.control.attrBool("onSelectAll", false)!
-                  ? (selected) {
-                      server.sendPageEvent(
-                          eventTarget: widget.control.id,
-                          eventName: "select_all",
-                          eventData:
-                              selected != null ? selected.toString() : "");
-                    }
-                  : null,
-              columns: viewModel.controlViews
-                  .where((c) => c.control.type == "c")
-                  .map((column) {
-                var labelCtrls = column.children.where((c) => c.name == "l");
-                return DataColumn(
-                    numeric: column.control.attrBool("numeric", false)!,
-                    tooltip: column.control.attrString("tooltip"),
-                    onSort: column.control.attrBool("onSort", false)!
-                        ? (columnIndex, ascending) {
-                            server.sendPageEvent(
-                                eventTarget: column.control.id,
-                                eventName: "sort",
-                                eventData: json.encode(
-                                    {"i": columnIndex, "a": ascending}));
-                          }
-                        : null,
-                    label: createControl(column.control, labelCtrls.first.id,
-                        column.control.isDisabled || tableDisabled));
-              }).toList(),
-              rows: viewModel.controlViews
-                  .where((c) => c.control.type == "r")
-                  .map((row) {
-                return DataRow(
-                    key: ValueKey(row.control.id),
-                    selected: row.control.attrBool("selected", false)!,
-                    color: parseMaterialStateColor(
-                        Theme.of(context), row.control, "color"),
-                    onSelectChanged:
-                        row.control.attrBool("onSelectChanged", false)!
-                            ? (selected) {
-                                server.sendPageEvent(
-                                    eventTarget: row.control.id,
-                                    eventName: "select_changed",
-                                    eventData: selected != null
-                                        ? selected.toString()
-                                        : "");
-                              }
-                            : null,
-                    onLongPress: row.control.attrBool("onLongPress", false)!
-                        ? () {
-                            server.sendPageEvent(
-                                eventTarget: row.control.id,
-                                eventName: "long_press",
-                                eventData: "");
-                          }
-                        : null,
-                    cells: row.children
-                        .map((cell) => DataCell(
-                              createControl(row.control, cell.childIds.first,
-                                  row.control.isDisabled || tableDisabled),
-                              placeholder: cell.attrBool("placeholder", false)!,
-                              showEditIcon:
-                                  cell.attrBool("showEditIcon", false)!,
-                              onDoubleTap: cell.attrBool("onDoubleTap", false)!
-                                  ? () {
-                                      server.sendPageEvent(
-                                          eventTarget: cell.id,
-                                          eventName: "double_tap",
-                                          eventData: "");
-                                    }
-                                  : null,
-                              onLongPress: cell.attrBool("onLongPress", false)!
-                                  ? () {
-                                      server.sendPageEvent(
-                                          eventTarget: cell.id,
-                                          eventName: "long_press",
-                                          eventData: "");
-                                    }
-                                  : null,
-                              onTap: cell.attrBool("onTap", false)!
-                                  ? () {
-                                      server.sendPageEvent(
-                                          eventTarget: cell.id,
-                                          eventName: "tap",
-                                          eventData: "");
-                                    }
-                                  : null,
-                              onTapCancel: cell.attrBool("onTapCancel", false)!
-                                  ? () {
-                                      server.sendPageEvent(
-                                          eventTarget: cell.id,
-                                          eventName: "tap_cancel",
-                                          eventData: "");
-                                    }
-                                  : null,
-                              onTapDown: cell.attrBool("onTapDown", false)!
-                                  ? (details) {
-                                      server.sendPageEvent(
-                                          eventTarget: cell.id,
-                                          eventName: "tap_down",
-                                          eventData: json.encode({
-                                            "kind": details.kind?.name,
-                                            "lx": details.localPosition.dx,
-                                            "ly": details.localPosition.dy,
-                                            "gx": details.globalPosition.dx,
-                                            "gy": details.globalPosition.dy,
-                                          }));
-                                    }
-                                  : null,
-                            ))
-                        .toList());
-              }).toList());
-        });
+      return DataTable(
+          decoration: decoration,
+          border: tableBorder,
+          checkboxHorizontalMargin:
+              widget.control.attrDouble("checkboxHorizontalMargin"),
+          columnSpacing: widget.control.attrDouble("columnSpacing"),
+          dataRowColor: parseMaterialStateColor(
+              Theme.of(context), widget.control, "dataRowColor"),
+          dataRowMinHeight: widget.control.attrDouble("dataRowMinHeight"),
+          dataRowMaxHeight: widget.control.attrDouble("dataRowMaxHeight"),
+          dataTextStyle: parseTextStyle(
+              Theme.of(context), widget.control, "dataTextStyle"),
+          headingRowColor: parseMaterialStateColor(
+              Theme.of(context), widget.control, "headingRowColor"),
+          headingRowHeight: widget.control.attrDouble("headingRowHeight"),
+          headingTextStyle: parseTextStyle(
+              Theme.of(context), widget.control, "headingTextStyle"),
+          dividerThickness: widget.control.attrDouble("dividerThickness"),
+          horizontalMargin: widget.control.attrDouble("horizontalMargin"),
+          showBottomBorder: widget.control.attrBool("showBottomBorder", false)!,
+          showCheckboxColumn:
+              widget.control.attrBool("showCheckboxColumn", false)!,
+          sortAscending: widget.control.attrBool("sortAscending", false)!,
+          sortColumnIndex: widget.control.attrInt("sortColumnIndex"),
+          onSelectAll: widget.control.attrBool("onSelectAll", false)!
+              ? (selected) {
+                  sendControlEvent(widget.control.id, "select_all",
+                      selected != null ? selected.toString() : "");
+                }
+              : null,
+          columns: viewModel.controlViews
+              .where((c) => c.control.type == "c")
+              .map((column) {
+            var labelCtrls = column.children.where((c) => c.name == "l");
+            return DataColumn(
+                numeric: column.control.attrBool("numeric", false)!,
+                tooltip: column.control.attrString("tooltip"),
+                onSort: column.control.attrBool("onSort", false)!
+                    ? (columnIndex, ascending) {
+                        sendControlEvent(column.control.id, "sort",
+                            json.encode({"i": columnIndex, "a": ascending}));
+                      }
+                    : null,
+                label: createControl(column.control, labelCtrls.first.id,
+                    column.control.isDisabled || tableDisabled));
+          }).toList(),
+          rows: viewModel.controlViews
+              .where((c) => c.control.type == "r")
+              .map((row) {
+            return DataRow(
+                key: ValueKey(row.control.id),
+                selected: row.control.attrBool("selected", false)!,
+                color: parseMaterialStateColor(
+                    Theme.of(context), row.control, "color"),
+                onSelectChanged: row.control.attrBool("onSelectChanged", false)!
+                    ? (selected) {
+                        sendControlEvent(row.control.id, "select_changed",
+                            selected != null ? selected.toString() : "");
+                      }
+                    : null,
+                onLongPress: row.control.attrBool("onLongPress", false)!
+                    ? () {
+                        sendControlEvent(row.control.id, "long_press", "");
+                      }
+                    : null,
+                cells: row.children
+                    .map((cell) => DataCell(
+                          createControl(row.control, cell.childIds.first,
+                              row.control.isDisabled || tableDisabled),
+                          placeholder: cell.attrBool("placeholder", false)!,
+                          showEditIcon: cell.attrBool("showEditIcon", false)!,
+                          onDoubleTap: cell.attrBool("onDoubleTap", false)!
+                              ? () {
+                                  sendControlEvent(cell.id, "double_tap", "");
+                                }
+                              : null,
+                          onLongPress: cell.attrBool("onLongPress", false)!
+                              ? () {
+                                  sendControlEvent(cell.id, "long_press", "");
+                                }
+                              : null,
+                          onTap: cell.attrBool("onTap", false)!
+                              ? () {
+                                  sendControlEvent(cell.id, "tap", "");
+                                }
+                              : null,
+                          onTapCancel: cell.attrBool("onTapCancel", false)!
+                              ? () {
+                                  sendControlEvent(cell.id, "tap_cancel", "");
+                                }
+                              : null,
+                          onTapDown: cell.attrBool("onTapDown", false)!
+                              ? (details) {
+                                  sendControlEvent(
+                                      cell.id,
+                                      "tap_down",
+                                      json.encode({
+                                        "kind": details.kind?.name,
+                                        "lx": details.localPosition.dx,
+                                        "ly": details.localPosition.dy,
+                                        "gx": details.globalPosition.dx,
+                                        "gy": details.globalPosition.dy,
+                                      }));
+                                }
+                              : null,
+                        ))
+                    .toList());
+          }).toList());
+    });
 
     return constrainedControl(
         context, datatable, widget.parent, widget.control);
diff --git a/package/lib/src/controls/date_picker.dart b/package/lib/src/controls/date_picker.dart
index 18f3a272a..5e99625b4 100644
--- a/package/lib/src/controls/date_picker.dart
+++ b/package/lib/src/controls/date_picker.dart
@@ -1,10 +1,8 @@
 import 'package:flutter/material.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/icons.dart';
+import 'flet_control_stateful_mixin.dart';
 import 'form_field.dart';
 
 class DatePickerControl extends StatefulWidget {
@@ -12,7 +10,6 @@ class DatePickerControl extends StatefulWidget {
   final Control control;
   final List<Control> children;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const DatePickerControl({
     super.key,
@@ -20,14 +17,14 @@ class DatePickerControl extends StatefulWidget {
     required this.control,
     required this.children,
     required this.parentDisabled,
-    required this.dispatch,
   });
 
   @override
   State<DatePickerControl> createState() => _DatePickerControlState();
 }
 
-class _DatePickerControlState extends State<DatePickerControl> {
+class _DatePickerControlState extends State<DatePickerControl>
+    with FletControlStatefulMixin {
   @override
   Widget build(BuildContext context) {
     debugPrint("DatePicker build: ${widget.control.id}");
@@ -64,8 +61,8 @@ class _DatePickerControlState extends State<DatePickerControl> {
     String? fieldLabelText = widget.control.attrString("fieldLabelText");
     IconData? switchToCalendarEntryModeIcon = parseIcon(
         widget.control.attrString("switchToCalendarEntryModeIcon", "")!);
-    IconData? switchToInputEntryModeIcon = parseIcon(
-        widget.control.attrString("switchToInputEntryModeIcon", "")!);
+    IconData? switchToInputEntryModeIcon =
+        parseIcon(widget.control.attrString("switchToInputEntryModeIcon", "")!);
 
     //Locale locale;
     // if (localeString == null) {
@@ -86,17 +83,9 @@ class _DatePickerControlState extends State<DatePickerControl> {
         eventName = "change";
       }
       widget.control.state["open"] = false;
-      List<Map<String, String>> props = [
-        {"i": widget.control.id, "value": stringValue, "open": "false"}
-      ];
-      widget.dispatch(
-          UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-      FletAppServices.of(context).server.updateControlProps(props: props);
-
-      FletAppServices.of(context).server.sendPageEvent(
-          eventTarget: widget.control.id,
-          eventName: eventName,
-          eventData: stringValue);
+      updateControlProps(
+          widget.control.id, {"value": stringValue, "open": "false"});
+      sendControlEvent(widget.control.id, eventName, stringValue);
     }
 
     Widget createSelectDateDialog() {
diff --git a/package/lib/src/controls/dismissible.dart b/package/lib/src/controls/dismissible.dart
index 39b760cb2..22faddb42 100644
--- a/package/lib/src/controls/dismissible.dart
+++ b/package/lib/src/controls/dismissible.dart
@@ -3,97 +3,97 @@ import 'dart:convert';
 
 import 'package:flutter/material.dart';
 
-import '../flet_server.dart';
 import '../models/control.dart';
 import '../utils/dismissible.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateful_mixin.dart';
 
-class DismissibleControl extends StatelessWidget {
+class DismissibleControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final List<Control> children;
   final bool parentDisabled;
-  final FletServer server;
 
   const DismissibleControl(
       {super.key,
       this.parent,
       required this.control,
       required this.children,
-      required this.parentDisabled,
-      required this.server});
+      required this.parentDisabled});
 
   @override
+  State<DismissibleControl> createState() => _DismissibleControlState();
+}
+
+class _DismissibleControlState extends State<DismissibleControl>
+    with FletControlStatefulMixin {
+  @override
   Widget build(BuildContext context) {
-    debugPrint("Dismissible build: ${control.id}");
+    debugPrint("Dismissible build: ${widget.control.id}");
 
-    bool disabled = control.isDisabled || parentDisabled;
-    var contentCtrls = children.where((c) => c.name == "content");
+    bool disabled = widget.control.isDisabled || widget.parentDisabled;
+    var contentCtrls = widget.children.where((c) => c.name == "content");
 
     if (contentCtrls.isEmpty) {
       return const ErrorControl("Dismissible does not have a content.");
     }
 
-    var backgroundCtrls = children.where((c) => c.name == "background");
+    var backgroundCtrls = widget.children.where((c) => c.name == "background");
 
     var secondaryBackgroundCtrls =
-        children.where((c) => c.name == "secondaryBackground");
+        widget.children.where((c) => c.name == "secondaryBackground");
 
     var dismissThresholds =
-        parseDismissThresholds(control, "dismissThresholds");
+        parseDismissThresholds(widget.control, "dismissThresholds");
 
     DismissDirection? direction = DismissDirection.values.firstWhere(
         (a) =>
             a.name.toLowerCase() ==
-            control.attrString("dismissDirection", "")!.toLowerCase(),
+            widget.control.attrString("dismissDirection", "")!.toLowerCase(),
         orElse: () => DismissDirection.horizontal);
 
-    server.controlInvokeMethods[control.id] = (methodName, args) async {
-      debugPrint("Dismissible.onMethod(${control.id})");
+    subscribeMethods(widget.control.id, (methodName, args) async {
+      debugPrint("Dismissible.onMethod(${widget.control.id})");
       if (methodName == "confirm_dismiss") {
-        control.state["confirm_dismiss"]
+        widget.control.state["confirm_dismiss"]
             ?.complete(bool.tryParse(args["dismiss"] ?? ""));
-        server.controlInvokeMethods.remove(control.id);
+        unsubscribeMethods(widget.control.id);
       }
 
       return null;
-    };
+    });
 
     return constrainedControl(
         context,
         Dismissible(
-            key: ValueKey<String>(control.id),
+            key: ValueKey<String>(widget.control.id),
             direction: direction,
             background: backgroundCtrls.isNotEmpty
-                ? createControl(control, backgroundCtrls.first.id, disabled)
+                ? createControl(
+                    widget.control, backgroundCtrls.first.id, disabled)
                 : Container(color: Colors.transparent),
             secondaryBackground: secondaryBackgroundCtrls.isNotEmpty
                 ? createControl(
-                    control, secondaryBackgroundCtrls.first.id, disabled)
+                    widget.control, secondaryBackgroundCtrls.first.id, disabled)
                 : Container(color: Colors.transparent),
-            onDismissed: control.attrBool("onDismiss", false)!
+            onDismissed: widget.control.attrBool("onDismiss", false)!
                 ? (DismissDirection direction) {
-                    server.sendPageEvent(
-                        eventTarget: control.id,
-                        eventName: "dismiss",
-                        eventData: direction.name);
+                    sendControlEvent(
+                        widget.control.id, "dismiss", direction.name);
                   }
                 : null,
-            onResize: control.attrBool("onResize", false)!
+            onResize: widget.control.attrBool("onResize", false)!
                 ? () {
-                    server.sendPageEvent(
-                        eventTarget: control.id,
-                        eventName: "resize",
-                        eventData: "");
+                    sendControlEvent(widget.control.id, "resize", "");
                   }
                 : null,
-            onUpdate: control.attrBool("onUpdate", false)!
+            onUpdate: widget.control.attrBool("onUpdate", false)!
                 ? (DismissUpdateDetails details) {
-                    server.sendPageEvent(
-                        eventTarget: control.id,
-                        eventName: "update",
-                        eventData: json.encode(DismissibleUpdateEvent(
+                    sendControlEvent(
+                        widget.control.id,
+                        "update",
+                        json.encode(DismissibleUpdateEvent(
                                 direction: details.direction.name,
                                 previousReached: details.previousReached,
                                 progress: details.progress,
@@ -101,27 +101,28 @@ class DismissibleControl extends StatelessWidget {
                             .toJson()));
                   }
                 : null,
-            confirmDismiss: control.attrBool("onConfirmDismiss", false)!
+            confirmDismiss: widget.control.attrBool("onConfirmDismiss", false)!
                 ? (DismissDirection direction) {
-                    debugPrint("Dismissible.confirmDismiss(${control.id})");
+                    debugPrint(
+                        "Dismissible.confirmDismiss(${widget.control.id})");
                     var completer = Completer<bool?>();
-                    control.state["confirm_dismiss"] = completer;
-                    server.sendPageEvent(
-                        eventTarget: control.id,
-                        eventName: "confirm_dismiss",
-                        eventData: direction.name);
+                    widget.control.state["confirm_dismiss"] = completer;
+                    sendControlEvent(
+                        widget.control.id, "confirm_dismiss", direction.name);
                     return completer.future;
                   }
                 : null,
-            movementDuration:
-                Duration(milliseconds: control.attrInt("duration", 200)!),
-            resizeDuration:
-                Duration(milliseconds: control.attrInt("resizeDuration", 300)!),
-            crossAxisEndOffset: control.attrDouble("crossAxisEndOffset", 0.0)!,
+            movementDuration: Duration(
+                milliseconds: widget.control.attrInt("duration", 200)!),
+            resizeDuration: Duration(
+                milliseconds: widget.control.attrInt("resizeDuration", 300)!),
+            crossAxisEndOffset:
+                widget.control.attrDouble("crossAxisEndOffset", 0.0)!,
             dismissThresholds: dismissThresholds ?? {},
-            child: createControl(control, contentCtrls.first.id, disabled)),
-        parent,
-        control);
+            child:
+                createControl(widget.control, contentCtrls.first.id, disabled)),
+        widget.parent,
+        widget.control);
   }
 }
 
diff --git a/package/lib/src/controls/drag_target.dart b/package/lib/src/controls/drag_target.dart
index a2fc45994..b43e79f48 100644
--- a/package/lib/src/controls/drag_target.dart
+++ b/package/lib/src/controls/drag_target.dart
@@ -2,13 +2,30 @@ import 'dart:convert';
 
 import 'package:flutter/material.dart';
 
-import '../flet_app_services.dart';
 import '../models/control.dart';
-import '../protocol/drag_target_accept_event.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateless_mixin.dart';
 
-class DragTargetControl extends StatelessWidget {
+class DragTargetAcceptEvent {
+  final String srcId;
+  final double x;
+  final double y;
+
+  DragTargetAcceptEvent({
+    required this.srcId,
+    required this.x,
+    required this.y,
+  });
+
+  Map<String, dynamic> toJson() => <String, dynamic>{
+        'src_id': srcId,
+        'x': x,
+        'y': y,
+      };
+}
+
+class DragTargetControl extends StatelessWidget with FletControlStatelessMixin {
   final Control? parent;
   final Control control;
   final List<Control> children;
@@ -38,8 +55,6 @@ class DragTargetControl extends StatelessWidget {
       return const ErrorControl("DragTarget should have content.");
     }
 
-    final server = FletAppServices.of(context).server;
-
     return DragTarget<String>(
       builder: (
         BuildContext context,
@@ -58,10 +73,8 @@ class DragTargetControl extends StatelessWidget {
           srcGroup = jd["group"] as String;
         }
         var groupsEqual = srcGroup == group;
-        server.sendPageEvent(
-            eventTarget: control.id,
-            eventName: "will_accept",
-            eventData: groupsEqual.toString());
+        sendControlEvent(
+            context, control.id, "will_accept", groupsEqual.toString());
         return groupsEqual;
       },
       onAcceptWithDetails: (details) {
@@ -69,10 +82,11 @@ class DragTargetControl extends StatelessWidget {
         debugPrint("DragTarget.onAcceptWithDetails ${control.id}: $data");
         var jd = json.decode(data);
         var srcId = jd["id"] as String;
-        server.sendPageEvent(
-            eventTarget: control.id,
-            eventName: "accept",
-            eventData: json.encode(DragTargetAcceptEvent(
+        sendControlEvent(
+            context,
+            control.id,
+            "accept",
+            json.encode(DragTargetAcceptEvent(
                     srcId: srcId, x: details.offset.dx, y: details.offset.dy)
                 .toJson()));
       },
@@ -83,8 +97,7 @@ class DragTargetControl extends StatelessWidget {
           var jd = json.decode(data);
           srcId = jd["id"] as String;
         }
-        server.sendPageEvent(
-            eventTarget: control.id, eventName: "leave", eventData: srcId);
+        sendControlEvent(context, control.id, "leave", srcId);
       },
     );
   }
diff --git a/package/lib/src/controls/dropdown.dart b/package/lib/src/controls/dropdown.dart
index c6ee23933..4b648dd53 100644
--- a/package/lib/src/controls/dropdown.dart
+++ b/package/lib/src/controls/dropdown.dart
@@ -1,37 +1,32 @@
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/control_children_view_model.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/alignment.dart';
 import '../utils/borders.dart';
 import '../utils/colors.dart';
 import '../utils/text.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
 import 'form_field.dart';
 
 class DropdownControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const DropdownControl(
       {super.key,
       this.parent,
       required this.control,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<DropdownControl> createState() => _DropdownControlState();
 }
 
-class _DropdownControlState extends State<DropdownControl> {
+class _DropdownControlState extends State<DropdownControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   String? _value;
   bool _focused = false;
   late final FocusNode _focusNode;
@@ -48,10 +43,8 @@ class _DropdownControlState extends State<DropdownControl> {
     setState(() {
       _focused = _focusNode.hasFocus;
     });
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   @override
@@ -65,136 +58,115 @@ class _DropdownControlState extends State<DropdownControl> {
   Widget build(BuildContext context) {
     debugPrint("Dropdown build: ${widget.control.id}");
 
-    final server = FletAppServices.of(context).server;
-
-    return StoreConnector<AppState, ControlChildrenViewModel>(
-        distinct: true,
-        ignoreChange: (state) {
-          return state.controls[widget.control.id] == null;
-        },
-        converter: (store) => ControlChildrenViewModel.fromStore(
-            store, widget.control.id,
-            dispatch: store.dispatch),
-        builder: (context, itemsView) {
-          debugPrint("Dropdown StoreConnector build: ${widget.control.id}");
-
-          bool autofocus = widget.control.attrBool("autofocus", false)!;
-          bool disabled = widget.control.isDisabled || widget.parentDisabled;
-
-          var textSize = widget.control.attrDouble("textSize");
-
-          var color = HexColor.fromString(
-              Theme.of(context), widget.control.attrString("color", "")!);
-          var focusedColor = HexColor.fromString(Theme.of(context),
-              widget.control.attrString("focusedColor", "")!);
-
-          TextStyle? textStyle =
-              parseTextStyle(Theme.of(context), widget.control, "textStyle");
-          if (textSize != null || color != null || focusedColor != null) {
-            textStyle = (textStyle ?? const TextStyle()).copyWith(
-                fontSize: textSize,
-                color: (_focused ? focusedColor ?? color : color) ??
-                    Theme.of(context).colorScheme.onSurface);
-          }
-
-          var alignment = parseAlignment(widget.control, "alignment");
-
-          var items = itemsView.children
-              .where((c) => c.name == null && c.isVisible)
-              .map<DropdownMenuItem<String>>((Control itemCtrl) {
-            Widget itemChild = Text(
-              itemCtrl.attrs["text"] ?? itemCtrl.attrs["key"] ?? itemCtrl.id,
-            );
-
-            if (alignment != null) {
-              itemChild = Container(alignment: alignment, child: itemChild);
+    return withControls(widget.control.childIds, (context, itemsView) {
+      debugPrint("DropdownFletControlState build: ${widget.control.id}");
+
+      bool autofocus = widget.control.attrBool("autofocus", false)!;
+      bool disabled = widget.control.isDisabled || widget.parentDisabled;
+
+      var textSize = widget.control.attrDouble("textSize");
+
+      var color = HexColor.fromString(
+          Theme.of(context), widget.control.attrString("color", "")!);
+      var focusedColor = HexColor.fromString(
+          Theme.of(context), widget.control.attrString("focusedColor", "")!);
+
+      TextStyle? textStyle =
+          parseTextStyle(Theme.of(context), widget.control, "textStyle");
+      if (textSize != null || color != null || focusedColor != null) {
+        textStyle = (textStyle ?? const TextStyle()).copyWith(
+            fontSize: textSize,
+            color: (_focused ? focusedColor ?? color : color) ??
+                Theme.of(context).colorScheme.onSurface);
+      }
+
+      var alignment = parseAlignment(widget.control, "alignment");
+
+      var items = itemsView.controlViews
+          .map((v) => v.control)
+          .where((c) => c.name == null && c.isVisible)
+          .map<DropdownMenuItem<String>>((Control itemCtrl) {
+        Widget itemChild = Text(
+          itemCtrl.attrs["text"] ?? itemCtrl.attrs["key"] ?? itemCtrl.id,
+        );
+
+        if (alignment != null) {
+          itemChild = Container(alignment: alignment, child: itemChild);
+        }
+        return DropdownMenuItem<String>(
+          enabled: !(disabled || itemCtrl.isDisabled),
+          value: itemCtrl.attrs["key"] ?? itemCtrl.attrs["text"] ?? itemCtrl.id,
+          child: itemChild,
+        );
+      }).toList();
+
+      String? value = widget.control.attrString("value");
+      if (_value != value) {
+        _value = value;
+      }
+
+      if (items.where((item) => item.value == value).isEmpty) {
+        _value = null;
+      }
+
+      var prefixControls = itemsView.controlViews
+          .where((c) => c.control.name == "prefix" && c.control.isVisible);
+      var suffixControls = itemsView.controlViews
+          .where((c) => c.control.name == "suffix" && c.control.isVisible);
+
+      var focusValue = widget.control.attrString("focus");
+      if (focusValue != null && focusValue != _lastFocusValue) {
+        _lastFocusValue = focusValue;
+        _focusNode.requestFocus();
+      }
+
+      var borderRadius = parseBorderRadius(widget.control, "borderRadius");
+
+      Widget dropDown = DropdownButtonFormField<String>(
+        style: textStyle,
+        autofocus: autofocus,
+        focusNode: _focusNode,
+        value: _value,
+        borderRadius: borderRadius,
+        alignment: alignment ?? AlignmentDirectional.centerStart,
+        isExpanded: alignment != null,
+        decoration: buildInputDecoration(
+            context,
+            widget.control,
+            prefixControls.isNotEmpty ? prefixControls.first.control : null,
+            suffixControls.isNotEmpty ? suffixControls.first.control : null,
+            null,
+            _focused),
+        onChanged: disabled
+            ? null
+            : (String? value) {
+                debugPrint("Dropdown selected value: $value");
+                _value = value!;
+                updateControlProps(widget.control.id, {"value": value});
+                sendControlEvent(widget.control.id, "change", value);
+              },
+        items: items,
+      );
+
+      if (widget.control.attrInt("expand", 0)! > 0) {
+        return constrainedControl(
+            context, dropDown, widget.parent, widget.control);
+      } else {
+        return LayoutBuilder(
+          builder: (BuildContext context, BoxConstraints constraints) {
+            if (constraints.maxWidth == double.infinity &&
+                widget.control.attrDouble("width") == null) {
+              dropDown = ConstrainedBox(
+                constraints: const BoxConstraints.tightFor(width: 300),
+                child: dropDown,
+              );
             }
-            return DropdownMenuItem<String>(
-              enabled: !(disabled || itemCtrl.isDisabled),
-              value: itemCtrl.attrs["key"] ??
-                  itemCtrl.attrs["text"] ??
-                  itemCtrl.id,
-              child: itemChild,
-            );
-          }).toList();
-
-          String? value = widget.control.attrString("value");
-          if (_value != value) {
-            _value = value;
-          }
-
-          if (items.where((item) => item.value == value).isEmpty) {
-            _value = null;
-          }
-
-          var prefixControls = itemsView.children
-              .where((c) => c.name == "prefix" && c.isVisible);
-          var suffixControls = itemsView.children
-              .where((c) => c.name == "suffix" && c.isVisible);
-
-          var focusValue = widget.control.attrString("focus");
-          if (focusValue != null && focusValue != _lastFocusValue) {
-            _lastFocusValue = focusValue;
-            _focusNode.requestFocus();
-          }
-
-          var borderRadius = parseBorderRadius(widget.control, "borderRadius");
-
-          Widget dropDown = DropdownButtonFormField<String>(
-            style: textStyle,
-            autofocus: autofocus,
-            focusNode: _focusNode,
-            value: _value,
-            borderRadius: borderRadius,
-            alignment: alignment ?? AlignmentDirectional.centerStart,
-            isExpanded: alignment != null,
-            decoration: buildInputDecoration(
-                context,
-                widget.control,
-                prefixControls.isNotEmpty ? prefixControls.first : null,
-                suffixControls.isNotEmpty ? suffixControls.first : null,
-                null,
-                _focused),
-            onChanged: disabled
-                ? null
-                : (String? value) {
-                    debugPrint("Dropdown selected value: $value");
-                    setState(() {
-                      _value = value!;
-                    });
-                    List<Map<String, String>> props = [
-                      {"i": widget.control.id, "value": value!}
-                    ];
-                    widget.dispatch(UpdateControlPropsAction(
-                        UpdateControlPropsPayload(props: props)));
-                    server.updateControlProps(props: props);
-                    server.sendPageEvent(
-                        eventTarget: widget.control.id,
-                        eventName: "change",
-                        eventData: value);
-                  },
-            items: items,
-          );
-
-          if (widget.control.attrInt("expand", 0)! > 0) {
+
             return constrainedControl(
                 context, dropDown, widget.parent, widget.control);
-          } else {
-            return LayoutBuilder(
-              builder: (BuildContext context, BoxConstraints constraints) {
-                if (constraints.maxWidth == double.infinity &&
-                    widget.control.attrDouble("width") == null) {
-                  dropDown = ConstrainedBox(
-                    constraints: const BoxConstraints.tightFor(width: 300),
-                    child: dropDown,
-                  );
-                }
-
-                return constrainedControl(
-                    context, dropDown, widget.parent, widget.control);
-              },
-            );
-          }
-        });
+          },
+        );
+      }
+    });
   }
 }
diff --git a/package/lib/src/controls/elevated_button.dart b/package/lib/src/controls/elevated_button.dart
index 737750893..ca2d9d420 100644
--- a/package/lib/src/controls/elevated_button.dart
+++ b/package/lib/src/controls/elevated_button.dart
@@ -1,6 +1,5 @@
 import 'package:flutter/material.dart';
 
-import '../flet_app_services.dart';
 import '../models/control.dart';
 import '../utils/buttons.dart';
 import '../utils/colors.dart';
@@ -8,6 +7,7 @@ import '../utils/icons.dart';
 import '../utils/launch_url.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class ElevatedButtonControl extends StatefulWidget {
   final Control? parent;
@@ -26,7 +26,8 @@ class ElevatedButtonControl extends StatefulWidget {
   State<ElevatedButtonControl> createState() => _ElevatedButtonControlState();
 }
 
-class _ElevatedButtonControlState extends State<ElevatedButtonControl> {
+class _ElevatedButtonControlState extends State<ElevatedButtonControl>
+    with FletControlStatefulMixin {
   late final FocusNode _focusNode;
   String? _lastFocusValue;
 
@@ -45,18 +46,14 @@ class _ElevatedButtonControlState extends State<ElevatedButtonControl> {
   }
 
   void _onFocusChange() {
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   @override
   Widget build(BuildContext context) {
     debugPrint("Button build: ${widget.control.id}");
 
-    final server = FletAppServices.of(context).server;
-
     String text = widget.control.attrString("text", "")!;
     String url = widget.control.attrString("url", "")!;
     IconData? icon = parseIcon(widget.control.attrString("icon", "")!);
@@ -75,30 +72,21 @@ class _ElevatedButtonControlState extends State<ElevatedButtonControl> {
               openWebBrowser(url,
                   webWindowName: widget.control.attrString("urlTarget"));
             }
-            server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "click",
-                eventData: "");
+            sendControlEvent(widget.control.id, "click", "");
           }
         : null;
 
     Function()? onLongPressHandler = onLongPress && !disabled
         ? () {
             debugPrint("Button ${widget.control.id} long pressed!");
-            server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "long_press",
-                eventData: "");
+            sendControlEvent(widget.control.id, "long_press", "");
           }
         : null;
 
     Function(bool)? onHoverHandler = onHover && !disabled
         ? (state) {
             debugPrint("Button ${widget.control.id} hovered!");
-            server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "hover",
-                eventData: state.toString());
+            sendControlEvent(widget.control.id, "hover", state.toString());
           }
         : null;
 
diff --git a/package/lib/src/controls/expansion_panel.dart b/package/lib/src/controls/expansion_panel.dart
index a6c9e8e19..478e7dc9c 100644
--- a/package/lib/src/controls/expansion_panel.dart
+++ b/package/lib/src/controls/expansion_panel.dart
@@ -1,37 +1,32 @@
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/controls_view_model.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/colors.dart';
 import '../utils/edge_insets.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
 
 class ExpansionPanelListControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final List<Control> children;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const ExpansionPanelListControl(
       {super.key,
       this.parent,
       required this.control,
       required this.children,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<ExpansionPanelListControl> createState() =>
       _ExpansionPanelListControlState();
 }
 
-class _ExpansionPanelListControlState extends State<ExpansionPanelListControl> {
+class _ExpansionPanelListControlState extends State<ExpansionPanelListControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   @override
   Widget build(BuildContext context) {
     debugPrint("ExpansionPanelList build: ${widget.control.id}");
@@ -41,20 +36,9 @@ class _ExpansionPanelListControlState extends State<ExpansionPanelListControl> {
         .toList();
 
     void onChange(int index, bool isExpanded) {
-      List<Map<String, String>> props = [
-        {
-          "i": panels[index].id,
-          "expanded": isExpanded.toString().toLowerCase()
-        }
-      ];
-      widget.dispatch(
-          UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-      var server = FletAppServices.of(context).server;
-      server.updateControlProps(props: props);
-      server.sendPageEvent(
-          eventTarget: widget.control.id,
-          eventName: "change",
-          eventData: "$index");
+      updateControlProps(
+          panels[index].id, {"expanded": isExpanded.toString().toLowerCase()});
+      sendControlEvent(widget.control.id, "change", "$index");
     }
 
     bool disabled = widget.control.isDisabled || widget.parentDisabled;
@@ -68,55 +52,50 @@ class _ExpansionPanelListControlState extends State<ExpansionPanelListControl> {
         parseEdgeInsets(widget.control, "expandedHeaderPadding");
 
     debugPrint(
-        "ExpansionPanelListControl StoreConnector build: ${widget.control.id}");
+        "ExpansionPanelListControlFletControlState build: ${widget.control.id}");
 
-    var panelList = StoreConnector<AppState, ControlsViewModel>(
-        distinct: true,
-        converter: (store) =>
-            ControlsViewModel.fromStore(store, panels.map((p) => p.id)),
-        builder: (content, panelViews) {
-          return ExpansionPanelList(
-              elevation: widget.control.attrDouble("elevation", 2)!,
-              materialGapSize: widget.control.attrDouble("spacing", 16)!,
-              dividerColor: dividerColor,
-              expandIconColor: expandedIconColor,
-              expandedHeaderPadding: expandedHeaderPadding ??
-                  const EdgeInsets.symmetric(vertical: 16),
-              expansionCallback: !disabled
-                  ? (int index, bool isExpanded) {
-                      onChange(index, isExpanded);
-                    }
-                  : null,
-              children: panelViews.controlViews.map((panelView) {
-                var headerCtrls = panelView.children
-                    .where((c) => c.name == "header" && c.isVisible);
-                var bodyCtrls = panelView.children
-                    .where((c) => c.name == "content" && c.isVisible);
+    var panelList =
+        withControls(panels.map((p) => p.id), (content, panelViews) {
+      return ExpansionPanelList(
+          elevation: widget.control.attrDouble("elevation", 2)!,
+          materialGapSize: widget.control.attrDouble("spacing", 16)!,
+          dividerColor: dividerColor,
+          expandIconColor: expandedIconColor,
+          expandedHeaderPadding:
+              expandedHeaderPadding ?? const EdgeInsets.symmetric(vertical: 16),
+          expansionCallback: !disabled
+              ? (int index, bool isExpanded) {
+                  onChange(index, isExpanded);
+                }
+              : null,
+          children: panelViews.controlViews.map((panelView) {
+            var headerCtrls = panelView.children
+                .where((c) => c.name == "header" && c.isVisible);
+            var bodyCtrls = panelView.children
+                .where((c) => c.name == "content" && c.isVisible);
 
-                var isExpanded =
-                    panelView.control.attrBool("expanded", false)!;
-                var canTapHeader =
-                    panelView.control.attrBool("canTapHeader", false)!;
-                var bgColor = HexColor.fromString(Theme.of(context),
-                    panelView.control.attrString("bgColor", "")!);
+            var isExpanded = panelView.control.attrBool("expanded", false)!;
+            var canTapHeader =
+                panelView.control.attrBool("canTapHeader", false)!;
+            var bgColor = HexColor.fromString(Theme.of(context),
+                panelView.control.attrString("bgColor", "")!);
 
-                return ExpansionPanel(
-                  backgroundColor: bgColor,
-                  isExpanded: isExpanded,
-                  canTapOnHeader: canTapHeader,
-                  headerBuilder: (BuildContext context, bool isExpanded) {
-                    return headerCtrls.isNotEmpty
-                        ? createControl(
-                            widget.control, headerCtrls.first.id, disabled)
-                        : const ListTile(title: Text("Header Placeholder"));
-                  },
-                  body: bodyCtrls.isNotEmpty
-                      ? createControl(
-                          widget.control, bodyCtrls.first.id, disabled)
-                      : const ListTile(title: Text("Body Placeholder")),
-                );
-              }).toList());
-        });
+            return ExpansionPanel(
+              backgroundColor: bgColor,
+              isExpanded: isExpanded,
+              canTapOnHeader: canTapHeader,
+              headerBuilder: (BuildContext context, bool isExpanded) {
+                return headerCtrls.isNotEmpty
+                    ? createControl(
+                        widget.control, headerCtrls.first.id, disabled)
+                    : const ListTile(title: Text("Header Placeholder"));
+              },
+              body: bodyCtrls.isNotEmpty
+                  ? createControl(widget.control, bodyCtrls.first.id, disabled)
+                  : const ListTile(title: Text("Body Placeholder")),
+            );
+          }).toList());
+    });
 
     return constrainedControl(
         context, panelList, widget.parent, widget.control);
diff --git a/package/lib/src/controls/expansion_tile.dart b/package/lib/src/controls/expansion_tile.dart
index cb2bdd193..406d92b5b 100644
--- a/package/lib/src/controls/expansion_tile.dart
+++ b/package/lib/src/controls/expansion_tile.dart
@@ -1,6 +1,5 @@
 import 'package:flutter/material.dart';
 
-import '../flet_app_services.dart';
 import '../models/control.dart';
 import '../utils/alignment.dart';
 import '../utils/borders.dart';
@@ -8,8 +7,10 @@ import '../utils/colors.dart';
 import '../utils/edge_insets.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateless_mixin.dart';
 
-class ExpansionTileControl extends StatelessWidget {
+class ExpansionTileControl extends StatelessWidget
+    with FletControlStatelessMixin {
   final Control? parent;
   final Control control;
   final List<Control> children;
@@ -26,8 +27,6 @@ class ExpansionTileControl extends StatelessWidget {
   Widget build(BuildContext context) {
     debugPrint("ExpansionTile build: ${control.id}");
 
-    final server = FletAppServices.of(context).server;
-
     var ctrls = children.where((c) => c.name == "controls" && c.isVisible);
     var leadingCtrls =
         children.where((c) => c.name == "leading" && c.isVisible);
@@ -77,17 +76,14 @@ class ExpansionTileControl extends StatelessWidget {
       return const ErrorControl(
           'CrossAxisAlignment.baseline is not supported since the expanded '
           'controls are aligned in a column, not a row. '
-              'Try aligning the controls differently.');
+          'Try aligning the controls differently.');
     }
 
     Function(bool)? onChange = (onchange) && !disabled
         ? (expanded) {
             debugPrint(
                 "ExpansionTile ${control.id} was ${expanded ? "expanded" : "collapsed"}");
-            server.sendPageEvent(
-                eventTarget: control.id,
-                eventName: "change",
-                eventData: "$expanded");
+            sendControlEvent(context, control.id, "change", "$expanded");
           }
         : null;
 
diff --git a/package/lib/src/controls/file_picker.dart b/package/lib/src/controls/file_picker.dart
index 4ead8ba43..2a0ada2d8 100644
--- a/package/lib/src/controls/file_picker.dart
+++ b/package/lib/src/controls/file_picker.dart
@@ -4,20 +4,62 @@ import 'package:collection/collection.dart';
 import 'package:file_picker/file_picker.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 import 'package:http/http.dart' as http;
 
-import '../actions.dart';
 import '../flet_app_services.dart';
 import '../flet_server.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../protocol/file_picker_result_event.dart';
-import '../protocol/file_picker_upload_file.dart';
-import '../protocol/file_picker_upload_progress_event.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/desktop.dart';
 import '../utils/strings.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
+
+class FilePickerResultEvent {
+  final String? path;
+  final List<FilePickerFile>? files;
+
+  FilePickerResultEvent({required this.path, required this.files});
+
+  Map<String, dynamic> toJson() => <String, dynamic>{
+        'path': path,
+        'files': files?.map((f) => f.toJson()).toList()
+      };
+}
+
+class FilePickerFile {
+  final String name;
+  final String? path;
+  final int size;
+
+  FilePickerFile({required this.name, required this.path, required this.size});
+
+  Map<String, dynamic> toJson() =>
+      <String, dynamic>{'name': name, 'path': path, 'size': size};
+}
+
+class FilePickerUploadFile {
+  final String name;
+  final String uploadUrl;
+  final String method;
+
+  FilePickerUploadFile(
+      {required this.name, required this.uploadUrl, required this.method});
+}
+
+class FilePickerUploadProgressEvent {
+  final String name;
+  final double? progress;
+  final String? error;
+
+  FilePickerUploadProgressEvent(
+      {required this.name, required this.progress, required this.error});
+
+  Map<String, dynamic> toJson() => <String, dynamic>{
+        'file_name': name,
+        'progress': progress,
+        'error': error
+      };
+}
 
 class FilePickerControl extends StatefulWidget {
   final Control? parent;
@@ -34,7 +76,8 @@ class FilePickerControl extends StatefulWidget {
   State<FilePickerControl> createState() => _FilePickerControlState();
 }
 
-class _FilePickerControlState extends State<FilePickerControl> {
+class _FilePickerControlState extends State<FilePickerControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   String? _state;
   String? _upload;
   String? _path;
@@ -44,128 +87,117 @@ class _FilePickerControlState extends State<FilePickerControl> {
   Widget build(BuildContext context) {
     debugPrint("FilePicker build: ${widget.control.id}");
 
-    return StoreConnector<AppState, Uri?>(
-        distinct: true,
-        converter: (store) => store.state.pageUri,
-        builder: (context, pageUri) {
-          var state = widget.control.attrString("state");
-          var upload = widget.control.attrString("upload");
-          var dialogTitle = widget.control.attrString("dialogTitle");
-          var fileName = widget.control.attrString("fileName");
-          var initialDirectory = widget.control.attrString("initialDirectory");
-          var allowMultiple = widget.control.attrBool("allowMultiple", false)!;
-          var allowedExtensions =
-              parseStringList(widget.control, "allowedExtensions");
-          FileType fileType = FileType.values.firstWhere(
-              (m) =>
-                  m.name.toLowerCase() ==
-                  widget.control.attrString("fileType", "")!.toLowerCase(),
-              orElse: () => FileType.any);
-          if (allowedExtensions != null && allowedExtensions.isNotEmpty) {
-            fileType = FileType.custom;
-          }
-
-          debugPrint("FilePicker _state: $_state, state: $state");
-
-          resetDialogState() {
-            _state = null;
-            var fletServices = FletAppServices.of(context);
-            List<Map<String, String>> props = [
-              {"i": widget.control.id, "state": ""}
-            ];
-            fletServices.store.dispatch(UpdateControlPropsAction(
-                UpdateControlPropsPayload(props: props)));
-            fletServices.server.updateControlProps(props: props);
-          }
-
-          sendEvent() {
-            if (defaultTargetPlatform != TargetPlatform.windows ||
-                !isDesktop()) {
-              resetDialogState();
-            }
-            var fletServices = FletAppServices.of(context);
-            fletServices.server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "result",
-                eventData: json.encode(FilePickerResultEvent(
-                    path: _path,
-                    files: _files
-                        ?.map((f) => FilePickerFile(
-                            name: f.name,
-                            path: kIsWeb ? null : f.path,
-                            size: f.size))
-                        .toList())));
-          }
-
-          if (_state != state) {
-            _path = null;
-            _files = null;
-            _state = state;
-
-            if (isDesktop() &&
-                defaultTargetPlatform == TargetPlatform.windows) {
-              resetDialogState();
-            }
-
-            // pickFiles
-            if (state?.toLowerCase() == "pickfiles") {
-              FilePicker.platform
-                  .pickFiles(
-                      dialogTitle: dialogTitle,
-                      initialDirectory: initialDirectory,
-                      lockParentWindow: true,
-                      type: fileType,
-                      allowedExtensions: allowedExtensions,
-                      allowMultiple: allowMultiple,
-                      withData: false,
-                      withReadStream: true)
-                  .then((result) {
-                debugPrint("pickFiles() completed");
-                _files = result?.files;
-                sendEvent();
-              });
-            }
-            // saveFile
-            else if (state?.toLowerCase() == "savefile" && !kIsWeb) {
-              FilePicker.platform
-                  .saveFile(
-                dialogTitle: dialogTitle,
-                fileName: fileName,
-                initialDirectory: initialDirectory,
-                lockParentWindow: true,
-                type: fileType,
-                allowedExtensions: allowedExtensions,
-              )
-                  .then((result) {
-                debugPrint("saveFile() completed");
-                _path = result;
-                sendEvent();
-              });
-            }
-            // saveFile
-            else if (state?.toLowerCase() == "getdirectorypath" && !kIsWeb) {
-              FilePicker.platform
-                  .getDirectoryPath(
-                dialogTitle: dialogTitle,
-                initialDirectory: initialDirectory,
-                lockParentWindow: true,
-              )
-                  .then((result) {
-                debugPrint("getDirectoryPath() completed");
-                _path = result;
-                sendEvent();
-              });
-            }
-          }
-
-          // upload files
-          if (_upload != upload && upload != null && _files != null) {
-            _upload = upload;
-            uploadFiles(upload, FletAppServices.of(context).server, pageUri!);
-          }
-
-          return widget.nextChild ?? const SizedBox.shrink();
-        });
+    return withPageArgs((context, pageArgs) {
+      var state = widget.control.attrString("state");
+      var upload = widget.control.attrString("upload");
+      var dialogTitle = widget.control.attrString("dialogTitle");
+      var fileName = widget.control.attrString("fileName");
+      var initialDirectory = widget.control.attrString("initialDirectory");
+      var allowMultiple = widget.control.attrBool("allowMultiple", false)!;
+      var allowedExtensions =
+          parseStringList(widget.control, "allowedExtensions");
+      FileType fileType = FileType.values.firstWhere(
+          (m) =>
+              m.name.toLowerCase() ==
+              widget.control.attrString("fileType", "")!.toLowerCase(),
+          orElse: () => FileType.any);
+      if (allowedExtensions != null && allowedExtensions.isNotEmpty) {
+        fileType = FileType.custom;
+      }
+
+      debugPrint("FilePicker _state: $_state, state: $state");
+
+      resetDialogState() {
+        _state = null;
+        updateControlProps(widget.control.id, {"state": ""});
+      }
+
+      sendEvent() {
+        if (defaultTargetPlatform != TargetPlatform.windows || !isDesktop()) {
+          resetDialogState();
+        }
+        sendControlEvent(
+            widget.control.id,
+            "result",
+            json.encode(FilePickerResultEvent(
+                path: _path,
+                files: _files
+                    ?.map((f) => FilePickerFile(
+                        name: f.name,
+                        path: kIsWeb ? null : f.path,
+                        size: f.size))
+                    .toList())));
+      }
+
+      if (_state != state) {
+        _path = null;
+        _files = null;
+        _state = state;
+
+        if (isDesktop() && defaultTargetPlatform == TargetPlatform.windows) {
+          resetDialogState();
+        }
+
+        // pickFiles
+        if (state?.toLowerCase() == "pickfiles") {
+          FilePicker.platform
+              .pickFiles(
+                  dialogTitle: dialogTitle,
+                  initialDirectory: initialDirectory,
+                  lockParentWindow: true,
+                  type: fileType,
+                  allowedExtensions: allowedExtensions,
+                  allowMultiple: allowMultiple,
+                  withData: false,
+                  withReadStream: true)
+              .then((result) {
+            debugPrint("pickFiles() completed");
+            _files = result?.files;
+            sendEvent();
+          });
+        }
+        // saveFile
+        else if (state?.toLowerCase() == "savefile" && !kIsWeb) {
+          FilePicker.platform
+              .saveFile(
+            dialogTitle: dialogTitle,
+            fileName: fileName,
+            initialDirectory: initialDirectory,
+            lockParentWindow: true,
+            type: fileType,
+            allowedExtensions: allowedExtensions,
+          )
+              .then((result) {
+            debugPrint("saveFile() completed");
+            _path = result;
+            sendEvent();
+          });
+        }
+        // saveFile
+        else if (state?.toLowerCase() == "getdirectorypath" && !kIsWeb) {
+          FilePicker.platform
+              .getDirectoryPath(
+            dialogTitle: dialogTitle,
+            initialDirectory: initialDirectory,
+            lockParentWindow: true,
+          )
+              .then((result) {
+            debugPrint("getDirectoryPath() completed");
+            _path = result;
+            sendEvent();
+          });
+        }
+      }
+
+      // upload files
+      if (_upload != upload && upload != null && _files != null) {
+        _upload = upload;
+        uploadFiles(
+            upload, FletAppServices.of(context).server, pageArgs.pageUri!);
+      }
+
+      return widget.nextChild ?? const SizedBox.shrink();
+    });
   }
 
   Future uploadFiles(String filesJson, FletServer server, Uri pageUri) async {
@@ -233,10 +265,10 @@ class _FilePickerControlState extends State<FilePickerControl> {
 
   void sendProgress(
       FletServer server, String name, double? progress, String? error) {
-    server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: "upload",
-        eventData: json.encode(FilePickerUploadProgressEvent(
+    sendControlEvent(
+        widget.control.id,
+        "upload",
+        json.encode(FilePickerUploadProgressEvent(
             name: name, progress: progress, error: error)));
   }
 
diff --git a/package/lib/src/controls/flet_control_stateful_mixin.dart b/package/lib/src/controls/flet_control_stateful_mixin.dart
new file mode 100644
index 000000000..090f2d151
--- /dev/null
+++ b/package/lib/src/controls/flet_control_stateful_mixin.dart
@@ -0,0 +1,37 @@
+import 'package:flutter/widgets.dart';
+
+import '../actions.dart';
+import '../flet_app_services.dart';
+import '../protocol/update_control_props_payload.dart';
+
+mixin FletControlStatefulMixin<T extends StatefulWidget> on State<T> {
+  void updateControlProps(String id, Map<String, String> props,
+      {bool clientOnly = false}) {
+    var appServices = FletAppServices.of(context);
+    var dispatch = appServices.store.dispatch;
+    Map<String, String> allProps = {"i": id};
+    for (var entry in props.entries) {
+      allProps[entry.key] = entry.value;
+    }
+    dispatch(
+        UpdateControlPropsAction(UpdateControlPropsPayload(props: [allProps])));
+    if (!clientOnly) {
+      appServices.server.updateControlProps(props: [allProps]);
+    }
+  }
+
+  void sendControlEvent(String controlId, String eventName, String eventData) {
+    FletAppServices.of(context).server.sendPageEvent(
+        eventTarget: controlId, eventName: eventName, eventData: eventData);
+  }
+
+  void subscribeMethods(String controlId,
+      Future<String?> Function(String, Map<String, String>) methodHandler) {
+    FletAppServices.of(context).server.controlInvokeMethods[controlId] =
+        methodHandler;
+  }
+
+  void unsubscribeMethods(String controlId) {
+    FletAppServices.of(context).server.controlInvokeMethods.remove(controlId);
+  }
+}
diff --git a/package/lib/src/controls/flet_control_stateless_mixin.dart b/package/lib/src/controls/flet_control_stateless_mixin.dart
new file mode 100644
index 000000000..a086a1eb0
--- /dev/null
+++ b/package/lib/src/controls/flet_control_stateless_mixin.dart
@@ -0,0 +1,11 @@
+import 'package:flutter/widgets.dart';
+
+import '../flet_app_services.dart';
+
+mixin FletControlStatelessMixin on StatelessWidget {
+  void sendControlEvent(BuildContext context, String controlId,
+      String eventName, String eventData) {
+    FletAppServices.of(context).server.sendPageEvent(
+        eventTarget: controlId, eventName: eventName, eventData: eventData);
+  }
+}
diff --git a/package/lib/src/controls/flet_store_mixin.dart b/package/lib/src/controls/flet_store_mixin.dart
new file mode 100644
index 000000000..24a69de2d
--- /dev/null
+++ b/package/lib/src/controls/flet_store_mixin.dart
@@ -0,0 +1,90 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_redux/flutter_redux.dart';
+
+import '../models/app_state.dart';
+import '../models/control.dart';
+import '../models/control_ancestor_view_model.dart';
+import '../models/control_tree_view_model.dart';
+import '../models/control_view_model.dart';
+import '../models/controls_view_model.dart';
+import '../models/page_args_model.dart';
+import '../models/page_size_view_model.dart';
+
+mixin FletStoreMixin {
+  Widget withPageArgs(Widget Function(BuildContext, PageArgsModel) build) {
+    return StoreConnector<AppState, PageArgsModel>(
+        distinct: true,
+        converter: (store) => PageArgsModel.fromStore(store),
+        builder: build);
+  }
+
+  Widget withPageSize(Widget Function(BuildContext, PageSizeViewModel) build) {
+    return StoreConnector<AppState, PageSizeViewModel>(
+        distinct: true,
+        converter: (store) => PageSizeViewModel.fromStore(store),
+        builder: build);
+  }
+
+  Widget withPagePlatform(Widget Function(BuildContext, TargetPlatform) build) {
+    return StoreConnector<AppState, TargetPlatform>(
+        distinct: true,
+        converter: (store) => TargetPlatform.values.firstWhere(
+            (a) =>
+                a.name.toLowerCase() ==
+                store.state.controls["page"]!
+                    .attrString("platform", "")!
+                    .toLowerCase(),
+            orElse: () => defaultTargetPlatform),
+        builder: build);
+  }
+
+  Widget withControl(
+      String id, Widget Function(BuildContext, ControlViewModel?) build) {
+    return StoreConnector<AppState, ControlViewModel?>(
+        distinct: true,
+        converter: (store) {
+          return ControlViewModel.fromStore(store, id);
+        },
+        ignoreChange: (state) {
+          return state.controls[id] == null;
+        },
+        builder: build);
+  }
+
+  Widget withControlTree(Control control,
+      Widget Function(BuildContext, ControlTreeViewModel) build) {
+    return StoreConnector<AppState, ControlTreeViewModel>(
+        distinct: true,
+        converter: (store) => ControlTreeViewModel.fromStore(store, control),
+        builder: build);
+  }
+
+  Widget withControlAncestor(String id, String ancestorType,
+      Widget Function(BuildContext, ControlAncestorViewModel) build) {
+    return StoreConnector<AppState, ControlAncestorViewModel>(
+        distinct: true,
+        converter: (store) =>
+            ControlAncestorViewModel.fromStore(store, id, ancestorType),
+        ignoreChange: (state) {
+          return state.controls[id] == null;
+        },
+        builder: build);
+  }
+
+  Widget withControls(Iterable<String> controlIds,
+      Widget Function(BuildContext, ControlsViewModel) build) {
+    return StoreConnector<AppState, ControlsViewModel>(
+        distinct: true,
+        converter: (store) => ControlsViewModel.fromStore(store, controlIds),
+        ignoreChange: (state) {
+          for (var id in controlIds) {
+            if (state.controls[id] == null) {
+              return true;
+            }
+          }
+          return false;
+        },
+        builder: build);
+  }
+}
diff --git a/package/lib/src/controls/floating_action_button.dart b/package/lib/src/controls/floating_action_button.dart
index 5b0c5cc75..a137bfc13 100644
--- a/package/lib/src/controls/floating_action_button.dart
+++ b/package/lib/src/controls/floating_action_button.dart
@@ -1,6 +1,5 @@
 import 'package:flutter/material.dart';
 
-import '../flet_app_services.dart';
 import '../models/control.dart';
 import '../utils/borders.dart';
 import '../utils/colors.dart';
@@ -9,8 +8,10 @@ import '../utils/launch_url.dart';
 import '../utils/transforms.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateless_mixin.dart';
 
-class FloatingActionButtonControl extends StatelessWidget {
+class FloatingActionButtonControl extends StatelessWidget
+    with FletControlStatelessMixin {
   final Control? parent;
   final Control control;
   final List<Control> children;
@@ -47,8 +48,7 @@ class FloatingActionButtonControl extends StatelessWidget {
             if (url != "") {
               openWebBrowser(url, webWindowName: urlTarget);
             }
-            FletAppServices.of(context).server.sendPageEvent(
-                eventTarget: control.id, eventName: "click", eventData: "");
+            sendControlEvent(context, control.id, "click", "");
           };
 
     if (text == null && icon == null && contentCtrls.isEmpty) {
diff --git a/package/lib/src/controls/gesture_detector.dart b/package/lib/src/controls/gesture_detector.dart
index 927c2d16c..c79c82bcb 100644
--- a/package/lib/src/controls/gesture_detector.dart
+++ b/package/lib/src/controls/gesture_detector.dart
@@ -4,11 +4,11 @@ import 'dart:convert';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 
-import '../flet_app_services.dart';
 import '../models/control.dart';
 import '../utils/mouse.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class GestureDetectorControl extends StatefulWidget {
   final Control? parent;
@@ -27,7 +27,8 @@ class GestureDetectorControl extends StatefulWidget {
   State<GestureDetectorControl> createState() => _GestureDetectorControlState();
 }
 
-class _GestureDetectorControlState extends State<GestureDetectorControl> {
+class _GestureDetectorControlState extends State<GestureDetectorControl>
+    with FletControlStatefulMixin {
   int _panTimestamp = DateTime.now().millisecondsSinceEpoch;
   double _panX = 0;
   double _panY = 0;
@@ -61,8 +62,6 @@ class _GestureDetectorControlState extends State<GestureDetectorControl> {
         widget.children.where((c) => c.name == "content" && c.isVisible);
     bool disabled = widget.control.isDisabled || widget.parentDisabled;
 
-    var server = FletAppServices.of(context).server;
-
     void sendEvent(String eventName, dynamic eventData) {
       var d = "";
       if (eventData is String) {
@@ -72,8 +71,7 @@ class _GestureDetectorControlState extends State<GestureDetectorControl> {
       }
 
       debugPrint("GestureDetector ${widget.control.id} $eventName");
-      server.sendPageEvent(
-          eventTarget: widget.control.id, eventName: eventName, eventData: d);
+      sendControlEvent(widget.control.id, eventName, d);
     }
 
     var onHover = widget.control.attrBool("onHover", false)!;
diff --git a/package/lib/src/controls/grid_view.dart b/package/lib/src/controls/grid_view.dart
index 95403508e..64eb6e3f1 100644
--- a/package/lib/src/controls/grid_view.dart
+++ b/package/lib/src/controls/grid_view.dart
@@ -13,15 +13,13 @@ class GridViewControl extends StatefulWidget {
   final Control control;
   final bool parentDisabled;
   final List<Control> children;
-  final dynamic dispatch;
 
   const GridViewControl(
       {super.key,
       this.parent,
       required this.control,
       required this.children,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<GridViewControl> createState() => _GridViewControlState();
@@ -99,7 +97,6 @@ class _GridViewControlState extends State<GridViewControl> {
         child = ScrollableControl(
           control: widget.control,
           scrollDirection: horizontal ? Axis.horizontal : Axis.vertical,
-          dispatch: widget.dispatch,
           scrollController: _controller,
           child: child,
         );
diff --git a/package/lib/src/controls/haptic_feedback.dart b/package/lib/src/controls/haptic_feedback.dart
index 53d153450..40c1f8023 100644
--- a/package/lib/src/controls/haptic_feedback.dart
+++ b/package/lib/src/controls/haptic_feedback.dart
@@ -1,9 +1,8 @@
 import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
 
-import '../flet_app_services.dart';
-import '../flet_server.dart';
 import '../models/control.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class HapticFeedbackControl extends StatefulWidget {
   final Control? parent;
@@ -20,12 +19,11 @@ class HapticFeedbackControl extends StatefulWidget {
   State<HapticFeedbackControl> createState() => _HapticFeedbackControlState();
 }
 
-class _HapticFeedbackControlState extends State<HapticFeedbackControl> {
-  FletServer? _server;
-
+class _HapticFeedbackControlState extends State<HapticFeedbackControl>
+    with FletControlStatefulMixin {
   @override
   void deactivate() {
-    _server?.controlInvokeMethods.remove(widget.control.id);
+    unsubscribeMethods(widget.control.id);
     super.deactivate();
   }
 
@@ -33,9 +31,7 @@ class _HapticFeedbackControlState extends State<HapticFeedbackControl> {
   Widget build(BuildContext context) {
     debugPrint("HapticFeedback build: ${widget.control.id}");
 
-    _server = FletAppServices.of(context).server;
-    _server?.controlInvokeMethods[widget.control.id] =
-        (methodName, args) async {
+    subscribeMethods(widget.control.id, (methodName, args) async {
       switch (methodName) {
         case "heavy_impact":
           HapticFeedback.heavyImpact();
@@ -51,7 +47,7 @@ class _HapticFeedbackControlState extends State<HapticFeedbackControl> {
           break;
       }
       return null;
-    };
+    });
 
     return widget.nextChild ?? const SizedBox.shrink();
   }
diff --git a/package/lib/src/controls/icon_button.dart b/package/lib/src/controls/icon_button.dart
index 52981eb58..1fcb1efad 100644
--- a/package/lib/src/controls/icon_button.dart
+++ b/package/lib/src/controls/icon_button.dart
@@ -1,6 +1,5 @@
 import 'package:flutter/material.dart';
 
-import '../flet_app_services.dart';
 import '../models/control.dart';
 import '../utils/buttons.dart';
 import '../utils/colors.dart';
@@ -8,6 +7,7 @@ import '../utils/icons.dart';
 import '../utils/launch_url.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class IconButtonControl extends StatefulWidget {
   final Control? parent;
@@ -26,7 +26,8 @@ class IconButtonControl extends StatefulWidget {
   State<IconButtonControl> createState() => _IconButtonControlState();
 }
 
-class _IconButtonControlState extends State<IconButtonControl> {
+class _IconButtonControlState extends State<IconButtonControl>
+    with FletControlStatefulMixin {
   late final FocusNode _focusNode;
   String? _lastFocusValue;
 
@@ -45,10 +46,8 @@ class _IconButtonControlState extends State<IconButtonControl> {
   }
 
   void _onFocusChange() {
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   @override
@@ -80,10 +79,7 @@ class _IconButtonControlState extends State<IconButtonControl> {
             if (url != "") {
               openWebBrowser(url, webWindowName: urlTarget);
             }
-            FletAppServices.of(context).server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "click",
-                eventData: "");
+            sendControlEvent(widget.control.id, "click", "");
           };
 
     Widget? button;
diff --git a/package/lib/src/controls/image.dart b/package/lib/src/controls/image.dart
index 74888e26d..13a2bd4b9 100644
--- a/package/lib/src/controls/image.dart
+++ b/package/lib/src/controls/image.dart
@@ -4,20 +4,20 @@ import 'dart:io' as io;
 import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 import 'package:flutter_svg/svg.dart';
 
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/page_args_model.dart';
 import '../utils/borders.dart';
 import '../utils/collections.dart';
 import '../utils/colors.dart';
 import '../utils/images.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateless_mixin.dart';
+import 'flet_store_mixin.dart';
 
-class ImageControl extends StatelessWidget {
+class ImageControl extends StatelessWidget
+    with FletControlStatelessMixin, FletStoreMixin {
   final Control? parent;
   final List<Control> children;
   final Control control;
@@ -57,43 +57,15 @@ class ImageControl extends StatelessWidget {
     var errorContentCtrls =
         children.where((c) => c.name == "error_content" && c.isVisible);
 
-    return StoreConnector<AppState, PageArgsModel>(
-        distinct: true,
-        converter: (store) => PageArgsModel.fromStore(store),
-        builder: (context, pageArgs) {
-          Widget? image;
+    return withPageArgs((context, pageArgs) {
+      Widget? image;
 
-          if (srcBase64 != "") {
-            try {
-              Uint8List bytes = base64Decode(srcBase64);
-              if (arrayIndexOf(
-                      bytes, Uint8List.fromList(utf8.encode(svgTag))) !=
-                  -1) {
-                image = SvgPicture.memory(bytes,
-                    width: width,
-                    height: height,
-                    fit: fit ?? BoxFit.contain,
-                    colorFilter: color != null
-                        ? ColorFilter.mode(
-                            color, colorBlendMode ?? BlendMode.srcIn)
-                        : null,
-                    semanticsLabel: semanticsLabel);
-              } else {
-                image = Image.memory(bytes,
-                    width: width,
-                    height: height,
-                    repeat: repeat,
-                    fit: fit,
-                    color: color,
-                    colorBlendMode: colorBlendMode,
-                    gaplessPlayback: gaplessPlayback ?? true,
-                    semanticLabel: semanticsLabel);
-              }
-            } catch (ex) {
-              return ErrorControl("Error decoding base64: ${ex.toString()}");
-            }
-          } else if (src.contains(svgTag)) {
-            image = SvgPicture.memory(Uint8List.fromList(utf8.encode(src)),
+      if (srcBase64 != "") {
+        try {
+          Uint8List bytes = base64Decode(srcBase64);
+          if (arrayIndexOf(bytes, Uint8List.fromList(utf8.encode(svgTag))) !=
+              -1) {
+            image = SvgPicture.memory(bytes,
                 width: width,
                 height: height,
                 fit: fit ?? BoxFit.contain,
@@ -102,74 +74,95 @@ class ImageControl extends StatelessWidget {
                     : null,
                 semanticsLabel: semanticsLabel);
           } else {
-            var assetSrc =
-                getAssetSrc(src, pageArgs.pageUri!, pageArgs.assetsDir);
+            image = Image.memory(bytes,
+                width: width,
+                height: height,
+                repeat: repeat,
+                fit: fit,
+                color: color,
+                colorBlendMode: colorBlendMode,
+                gaplessPlayback: gaplessPlayback ?? true,
+                semanticLabel: semanticsLabel);
+          }
+        } catch (ex) {
+          return ErrorControl("Error decoding base64: ${ex.toString()}");
+        }
+      } else if (src.contains(svgTag)) {
+        image = SvgPicture.memory(Uint8List.fromList(utf8.encode(src)),
+            width: width,
+            height: height,
+            fit: fit ?? BoxFit.contain,
+            colorFilter: color != null
+                ? ColorFilter.mode(color, colorBlendMode ?? BlendMode.srcIn)
+                : null,
+            semanticsLabel: semanticsLabel);
+      } else {
+        var assetSrc = getAssetSrc(src, pageArgs.pageUri!, pageArgs.assetsDir);
 
-            if (assetSrc.isFile) {
-              // from File
-              if (assetSrc.path.endsWith(".svg")) {
-                image = getSvgPictureFromFile(
-                    src: assetSrc.path,
-                    width: width,
-                    height: height,
-                    fit: fit ?? BoxFit.contain,
-                    color: color,
-                    blendMode: colorBlendMode ?? BlendMode.srcIn,
-                    semanticsLabel: semanticsLabel);
-              } else {
-                image = Image.file(
-                  io.File(assetSrc.path),
-                  width: width,
-                  height: height,
-                  repeat: repeat,
-                  fit: fit,
-                  color: color,
-                  gaplessPlayback: gaplessPlayback ?? false,
-                  colorBlendMode: colorBlendMode,
-                  semanticLabel: semanticsLabel,
-                  errorBuilder: errorContentCtrls.isNotEmpty
-                      ? (context, error, stackTrace) {
-                          return createControl(
-                              control, errorContentCtrls.first.id, disabled);
-                        }
-                      : null,
-                );
-              }
-            } else {
-              // URL
-              if (assetSrc.path.endsWith(".svg")) {
-                image = SvgPicture.network(assetSrc.path,
-                    width: width,
-                    height: height,
-                    fit: fit ?? BoxFit.contain,
-                    colorFilter: color != null
-                        ? ColorFilter.mode(
-                            color, colorBlendMode ?? BlendMode.srcIn)
-                        : null,
-                    semanticsLabel: semanticsLabel);
-              } else {
-                image = Image.network(assetSrc.path,
-                    width: width,
-                    height: height,
-                    repeat: repeat,
-                    fit: fit,
-                    color: color,
-                    gaplessPlayback: gaplessPlayback ?? false,
-                    colorBlendMode: colorBlendMode,
-                    semanticLabel: semanticsLabel,
-                    errorBuilder: errorContentCtrls.isNotEmpty
-                        ? (context, error, stackTrace) {
-                            return createControl(
-                                control, errorContentCtrls.first.id, disabled);
-                          }
-                        : null);
-              }
-            }
+        if (assetSrc.isFile) {
+          // from File
+          if (assetSrc.path.endsWith(".svg")) {
+            image = getSvgPictureFromFile(
+                src: assetSrc.path,
+                width: width,
+                height: height,
+                fit: fit ?? BoxFit.contain,
+                color: color,
+                blendMode: colorBlendMode ?? BlendMode.srcIn,
+                semanticsLabel: semanticsLabel);
+          } else {
+            image = Image.file(
+              io.File(assetSrc.path),
+              width: width,
+              height: height,
+              repeat: repeat,
+              fit: fit,
+              color: color,
+              gaplessPlayback: gaplessPlayback ?? false,
+              colorBlendMode: colorBlendMode,
+              semanticLabel: semanticsLabel,
+              errorBuilder: errorContentCtrls.isNotEmpty
+                  ? (context, error, stackTrace) {
+                      return createControl(
+                          control, errorContentCtrls.first.id, disabled);
+                    }
+                  : null,
+            );
+          }
+        } else {
+          // URL
+          if (assetSrc.path.endsWith(".svg")) {
+            image = SvgPicture.network(assetSrc.path,
+                width: width,
+                height: height,
+                fit: fit ?? BoxFit.contain,
+                colorFilter: color != null
+                    ? ColorFilter.mode(color, colorBlendMode ?? BlendMode.srcIn)
+                    : null,
+                semanticsLabel: semanticsLabel);
+          } else {
+            image = Image.network(assetSrc.path,
+                width: width,
+                height: height,
+                repeat: repeat,
+                fit: fit,
+                color: color,
+                gaplessPlayback: gaplessPlayback ?? false,
+                colorBlendMode: colorBlendMode,
+                semanticLabel: semanticsLabel,
+                errorBuilder: errorContentCtrls.isNotEmpty
+                    ? (context, error, stackTrace) {
+                        return createControl(
+                            control, errorContentCtrls.first.id, disabled);
+                      }
+                    : null);
           }
+        }
+      }
 
-          return constrainedControl(
-              context, _clipCorners(image, control), parent, control);
-        });
+      return constrainedControl(
+          context, _clipCorners(image, control), parent, control);
+    });
   }
 
   Widget _clipCorners(Widget image, Control control) {
diff --git a/package/lib/src/controls/linechart.dart b/package/lib/src/controls/linechart.dart
index a20946bcf..48b0707cc 100644
--- a/package/lib/src/controls/linechart.dart
+++ b/package/lib/src/controls/linechart.dart
@@ -1,18 +1,14 @@
 import 'dart:convert';
 
 import 'package:collection/collection.dart';
+import 'package:equatable/equatable.dart';
 import 'package:fl_chart/fl_chart.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_redux/flutter_redux.dart';
+import 'package:redux/redux.dart';
 
-import '../flet_app_services.dart';
 import '../models/app_state.dart';
-import '../models/chart_axis_view_model.dart';
 import '../models/control.dart';
-import '../models/linechart_data_point_view_model.dart';
-import '../models/linechart_data_view_model.dart';
-import '../models/linechart_event_data.dart';
-import '../models/linechart_view_model.dart';
 import '../utils/animations.dart';
 import '../utils/borders.dart';
 import '../utils/charts.dart';
@@ -21,7 +17,139 @@ import '../utils/gradient.dart';
 import '../utils/numbers.dart';
 import '../utils/shadows.dart';
 import '../utils/text.dart';
+import 'charts.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
+
+class LineChartDataPointViewModel extends Equatable {
+  final Control control;
+  final double x;
+  final double y;
+  final String? tooltip;
+
+  const LineChartDataPointViewModel(
+      {required this.control,
+      required this.x,
+      required this.y,
+      required this.tooltip});
+
+  static LineChartDataPointViewModel fromStore(
+      Store<AppState> store, Control control) {
+    return LineChartDataPointViewModel(
+        control: control,
+        x: control.attrDouble("x")!,
+        y: control.attrDouble("y")!,
+        tooltip: control.attrString("tooltip"));
+  }
+
+  @override
+  List<Object?> get props => [control];
+}
+
+class LineChartDataViewModel extends Equatable {
+  final Control control;
+  final List<LineChartDataPointViewModel> dataPoints;
+
+  const LineChartDataViewModel(
+      {required this.control, required this.dataPoints});
+
+  static LineChartDataViewModel fromStore(
+      Store<AppState> store, Control control) {
+    return LineChartDataViewModel(
+        control: control,
+        dataPoints: store.state.controls[control.id]!.childIds
+            .map((childId) => store.state.controls[childId])
+            .whereNotNull()
+            .where((c) => c.isVisible)
+            .map((c) => LineChartDataPointViewModel.fromStore(store, c))
+            .toList());
+  }
+
+  @override
+  List<Object?> get props => [control, dataPoints];
+}
+
+class LineChartEventData extends Equatable {
+  final String eventType;
+  final List<LineChartEventDataSpot> barSpots;
+
+  const LineChartEventData({required this.eventType, required this.barSpots});
+
+  Map<String, dynamic> toJson() => <String, dynamic>{
+        'type': eventType,
+        'spots': barSpots,
+      };
+
+  @override
+  List<Object?> get props => [eventType, barSpots];
+}
+
+class LineChartEventDataSpot extends Equatable {
+  final int barIndex;
+  final int spotIndex;
+
+  const LineChartEventDataSpot(
+      {required this.barIndex, required this.spotIndex});
+
+  Map<String, dynamic> toJson() => <String, dynamic>{
+        'bar_index': barIndex,
+        'spot_index': spotIndex,
+      };
+
+  @override
+  List<Object?> get props => [barIndex, spotIndex];
+}
+
+class LineChartViewModel extends Equatable {
+  final Control control;
+  final ChartAxisViewModel? leftAxis;
+  final ChartAxisViewModel? topAxis;
+  final ChartAxisViewModel? rightAxis;
+  final ChartAxisViewModel? bottomAxis;
+  final List<LineChartDataViewModel> dataSeries;
+
+  const LineChartViewModel(
+      {required this.control,
+      required this.leftAxis,
+      required this.topAxis,
+      required this.rightAxis,
+      required this.bottomAxis,
+      required this.dataSeries});
+
+  static LineChartViewModel fromStore(
+      Store<AppState> store, Control control, List<Control> children) {
+    var leftAxisCtrls =
+        children.where((c) => c.type == "axis" && c.name == "l" && c.isVisible);
+    var topAxisCtrls =
+        children.where((c) => c.type == "axis" && c.name == "t" && c.isVisible);
+    var rightAxisCtrls =
+        children.where((c) => c.type == "axis" && c.name == "r" && c.isVisible);
+    var bottomAxisCtrls =
+        children.where((c) => c.type == "axis" && c.name == "b" && c.isVisible);
+    return LineChartViewModel(
+        control: control,
+        leftAxis: leftAxisCtrls.isNotEmpty
+            ? ChartAxisViewModel.fromStore(store, leftAxisCtrls.first)
+            : null,
+        topAxis: topAxisCtrls.isNotEmpty
+            ? ChartAxisViewModel.fromStore(store, topAxisCtrls.first)
+            : null,
+        rightAxis: rightAxisCtrls.isNotEmpty
+            ? ChartAxisViewModel.fromStore(store, rightAxisCtrls.first)
+            : null,
+        bottomAxis: bottomAxisCtrls.isNotEmpty
+            ? ChartAxisViewModel.fromStore(store, bottomAxisCtrls.first)
+            : null,
+        dataSeries: children
+            .where((c) => c.type == "data" && c.isVisible)
+            .map((c) => LineChartDataViewModel.fromStore(store, c))
+            .toList());
+  }
+
+  @override
+  List<Object?> get props =>
+      [control, leftAxis, rightAxis, topAxis, bottomAxis, dataSeries];
+}
 
 class LineChartControl extends StatefulWidget {
   final Control? parent;
@@ -40,7 +168,8 @@ class LineChartControl extends StatefulWidget {
   State<LineChartControl> createState() => _LineChartControlState();
 }
 
-class _LineChartControlState extends State<LineChartControl> {
+class _LineChartControlState extends State<LineChartControl>
+    with FletControlStatefulMixin {
   LineChartEventData? _eventData;
 
   @override
@@ -242,10 +371,8 @@ class _LineChartControlState extends State<LineChartControl> {
                             _eventData = eventData;
                             debugPrint(
                                 "LineChart ${widget.control.id} ${eventData.eventType}");
-                            FletAppServices.of(context).server.sendPageEvent(
-                                eventTarget: widget.control.id,
-                                eventName: "chart_event",
-                                eventData: json.encode(eventData));
+                            sendControlEvent(widget.control.id, "chart_event",
+                                json.encode(eventData));
                           }
                         }
                       : null,
diff --git a/package/lib/src/controls/list_tile.dart b/package/lib/src/controls/list_tile.dart
index 800bdd2bd..0b013b47c 100644
--- a/package/lib/src/controls/list_tile.dart
+++ b/package/lib/src/controls/list_tile.dart
@@ -1,10 +1,10 @@
 import 'package:flutter/material.dart';
 
-import '../flet_app_services.dart';
 import '../models/control.dart';
 import '../utils/edge_insets.dart';
 import '../utils/launch_url.dart';
 import 'create_control.dart';
+import 'flet_control_stateless_mixin.dart';
 
 class ListTileClicks extends InheritedWidget {
   const ListTileClicks({
@@ -23,7 +23,7 @@ class ListTileClicks extends InheritedWidget {
   bool updateShouldNotify(ListTileClicks oldWidget) => true;
 }
 
-class ListTileControl extends StatelessWidget {
+class ListTileControl extends StatelessWidget with FletControlStatelessMixin {
   final Control? parent;
   final Control control;
   final List<Control> children;
@@ -41,8 +41,6 @@ class ListTileControl extends StatelessWidget {
   Widget build(BuildContext context) {
     debugPrint("ListTile build: ${control.id}");
 
-    final server = FletAppServices.of(context).server;
-
     var leadingCtrls =
         children.where((c) => c.name == "leading" && c.isVisible);
     var titleCtrls = children.where((c) => c.name == "title" && c.isVisible);
@@ -72,8 +70,7 @@ class ListTileControl extends StatelessWidget {
               openWebBrowser(url, webWindowName: urlTarget);
             }
             if (onclick) {
-              server.sendPageEvent(
-                  eventTarget: control.id, eventName: "click", eventData: "");
+              sendControlEvent(context, control.id, "click", "");
             }
           }
         : null;
@@ -81,10 +78,7 @@ class ListTileControl extends StatelessWidget {
     Function()? onLongPress = onLongPressDefined && !disabled
         ? () {
             debugPrint("Button ${control.id} clicked!");
-            server.sendPageEvent(
-                eventTarget: control.id,
-                eventName: "long_press",
-                eventData: "");
+            sendControlEvent(context, control.id, "long_press", "");
           }
         : null;
 
diff --git a/package/lib/src/controls/list_view.dart b/package/lib/src/controls/list_view.dart
index 784e2ce45..37a27cd4a 100644
--- a/package/lib/src/controls/list_view.dart
+++ b/package/lib/src/controls/list_view.dart
@@ -13,15 +13,13 @@ class ListViewControl extends StatefulWidget {
   final Control control;
   final bool parentDisabled;
   final List<Control> children;
-  final dynamic dispatch;
 
   const ListViewControl(
       {super.key,
       this.parent,
       required this.control,
       required this.children,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<ListViewControl> createState() => _ListViewControlState();
@@ -115,7 +113,6 @@ class _ListViewControlState extends State<ListViewControl> {
         child = ScrollableControl(
           control: widget.control,
           scrollDirection: horizontal ? Axis.horizontal : Axis.vertical,
-          dispatch: widget.dispatch,
           scrollController: _controller,
           child: child,
         );
diff --git a/package/lib/src/controls/markdown.dart b/package/lib/src/controls/markdown.dart
index 61cd55c3e..bd64fc7e9 100644
--- a/package/lib/src/controls/markdown.dart
+++ b/package/lib/src/controls/markdown.dart
@@ -1,24 +1,24 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_highlight/theme_map.dart';
 import 'package:flutter_markdown/flutter_markdown.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 import 'package:markdown/markdown.dart' as md;
 
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/page_args_model.dart';
 import '../utils/launch_url.dart';
 import '../utils/text.dart';
 import '../utils/uri.dart';
 import 'create_control.dart';
+import 'flet_control_stateless_mixin.dart';
+import 'flet_store_mixin.dart';
 import 'highlight_view.dart';
 
-class MarkdownControl extends StatelessWidget {
+class MarkdownControl extends StatelessWidget
+    with FletControlStatelessMixin, FletStoreMixin {
   final Control? parent;
   final Control control;
 
-  const MarkdownControl({super.key, required this.parent, required this.control});
+  const MarkdownControl(
+      {super.key, required this.parent, required this.control});
 
   @override
   Widget build(BuildContext context) {
@@ -52,35 +52,29 @@ class MarkdownControl extends StatelessWidget {
     var autoFollowLinks = control.attrBool("autoFollowLinks", false)!;
     var autoFollowLinksTarget = control.attrString("autoFollowLinksTarget");
 
-    return StoreConnector<AppState, PageArgsModel>(
-        distinct: true,
-        converter: (store) => PageArgsModel.fromStore(store),
-        builder: (context, pageArgs) {
-          Widget markdown = MarkdownBody(
-              data: value,
-              selectable: control.attrBool("selectable", false)!,
-              imageDirectory: pageArgs.assetsDir != ""
-                  ? pageArgs.assetsDir
-                  : getBaseUri(pageArgs.pageUri!).toString(),
-              extensionSet: extensionSet,
-              builders: {
-                'code':
-                    CodeElementBuilder(codeTheme.toLowerCase(), mdStyleSheet),
-              },
-              styleSheet: mdStyleSheet,
-              onTapLink: (String text, String? href, String title) {
-                debugPrint("Markdown link tapped ${control.id} clicked: $href");
-                if (autoFollowLinks && href != null) {
-                  openWebBrowser(href, webWindowName: autoFollowLinksTarget);
-                }
-                FletAppServices.of(context).server.sendPageEvent(
-                    eventTarget: control.id,
-                    eventName: "tap_link",
-                    eventData: href?.toString() ?? "");
-              });
-
-          return constrainedControl(context, markdown, parent, control);
-        });
+    return withPageArgs((context, pageArgs) {
+      Widget markdown = MarkdownBody(
+          data: value,
+          selectable: control.attrBool("selectable", false)!,
+          imageDirectory: pageArgs.assetsDir != ""
+              ? pageArgs.assetsDir
+              : getBaseUri(pageArgs.pageUri!).toString(),
+          extensionSet: extensionSet,
+          builders: {
+            'code': CodeElementBuilder(codeTheme.toLowerCase(), mdStyleSheet),
+          },
+          styleSheet: mdStyleSheet,
+          onTapLink: (String text, String? href, String title) {
+            debugPrint("Markdown link tapped ${control.id} clicked: $href");
+            if (autoFollowLinks && href != null) {
+              openWebBrowser(href, webWindowName: autoFollowLinksTarget);
+            }
+            sendControlEvent(
+                context, control.id, "tap_link", href?.toString() ?? "");
+          });
+
+      return constrainedControl(context, markdown, parent, control);
+    });
   }
 }
 
diff --git a/package/lib/src/controls/menu_item_button.dart b/package/lib/src/controls/menu_item_button.dart
index 9796c8994..e5cfb3d25 100644
--- a/package/lib/src/controls/menu_item_button.dart
+++ b/package/lib/src/controls/menu_item_button.dart
@@ -1,9 +1,9 @@
 import 'package:flutter/material.dart';
 
-import '../flet_app_services.dart';
 import '../models/control.dart';
 import '../utils/buttons.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class MenuItemButtonControl extends StatefulWidget {
   final Control? parent;
@@ -22,7 +22,8 @@ class MenuItemButtonControl extends StatefulWidget {
   State<MenuItemButtonControl> createState() => _MenuItemButtonControlState();
 }
 
-class _MenuItemButtonControlState extends State<MenuItemButtonControl> {
+class _MenuItemButtonControlState extends State<MenuItemButtonControl>
+    with FletControlStatefulMixin {
   late final FocusNode _focusNode;
   String? _lastFocusValue;
 
@@ -41,10 +42,8 @@ class _MenuItemButtonControlState extends State<MenuItemButtonControl> {
   }
 
   void _onFocusChange() {
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   @override
@@ -82,8 +81,6 @@ class _MenuItemButtonControlState extends State<MenuItemButtonControl> {
     bool onClick = widget.control.attrBool("onClick", false)!;
     bool onHover = widget.control.attrBool("onHover", false)!;
 
-    var server = FletAppServices.of(context).server;
-
     var menuItem = MenuItemButton(
       focusNode: _focusNode,
       clipBehavior: clipBehavior,
@@ -92,18 +89,12 @@ class _MenuItemButtonControlState extends State<MenuItemButtonControl> {
       requestFocusOnHover: widget.control.attrBool("focusOnHover", true)!,
       onHover: onHover && !disabled
           ? (bool value) {
-              server.sendPageEvent(
-                  eventTarget: widget.control.id,
-                  eventName: "hover",
-                  eventData: "$value");
+              sendControlEvent(widget.control.id, "hover", "$value");
             }
           : null,
       onPressed: onClick && !disabled
           ? () {
-              server.sendPageEvent(
-                  eventTarget: widget.control.id,
-                  eventName: "click",
-                  eventData: "");
+              sendControlEvent(widget.control.id, "click", "");
             }
           : null,
       leadingIcon: leading.isNotEmpty
diff --git a/package/lib/src/controls/navigation_bar.dart b/package/lib/src/controls/navigation_bar.dart
index 702dd9a66..36836d14f 100644
--- a/package/lib/src/controls/navigation_bar.dart
+++ b/package/lib/src/controls/navigation_bar.dart
@@ -1,138 +1,120 @@
 import 'package:collection/collection.dart';
-import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/controls_view_model.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/borders.dart';
 import '../utils/colors.dart';
 import '../utils/icons.dart';
 import 'create_control.dart';
 import 'cupertino_navigation_bar.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
 
 class NavigationBarControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final List<Control> children;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const NavigationBarControl(
       {super.key,
       this.parent,
       required this.control,
       required this.children,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<NavigationBarControl> createState() => _NavigationBarControlState();
 }
 
-class _NavigationBarControlState extends State<NavigationBarControl> {
+class _NavigationBarControlState extends State<NavigationBarControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   int _selectedIndex = 0;
 
   void _destinationChanged(int index) {
     _selectedIndex = index;
     debugPrint("Selected index: $_selectedIndex");
-    List<Map<String, String>> props = [
-      {"i": widget.control.id, "selectedindex": _selectedIndex.toString()}
-    ];
-    widget.dispatch(
-        UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-    final server = FletAppServices.of(context).server;
-    server.updateControlProps(props: props);
-    server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: "change",
-        eventData: _selectedIndex.toString());
+    updateControlProps(
+        widget.control.id, {"selectedindex": _selectedIndex.toString()});
+    sendControlEvent(widget.control.id, "change", _selectedIndex.toString());
   }
 
   @override
   Widget build(BuildContext context) {
     debugPrint("NavigationBarControl build: ${widget.control.id}");
 
-    bool adaptive = widget.control.attrBool("adaptive", false)!;
-    if (adaptive &&
-        (defaultTargetPlatform == TargetPlatform.iOS ||
-            defaultTargetPlatform == TargetPlatform.macOS)) {
-      return CupertinoNavigationBarControl(
-          control: widget.control,
-          children: widget.children,
-          parentDisabled: widget.parentDisabled,
-          dispatch: widget.dispatch);
-    }
-
-    bool disabled = widget.control.isDisabled || widget.parentDisabled;
-    var selectedIndex = widget.control.attrInt("selectedIndex", 0)!;
-
-    if (_selectedIndex != selectedIndex) {
-      _selectedIndex = selectedIndex;
-    }
-
-    NavigationDestinationLabelBehavior? labelBehavior =
-        NavigationDestinationLabelBehavior.values.firstWhereOrNull((a) =>
-            a.name.toLowerCase() ==
-            widget.control.attrString("labelBehavior", "")!.toLowerCase());
-
-    var navBar = StoreConnector<AppState, ControlsViewModel>(
-        distinct: true,
-        converter: (store) => ControlsViewModel.fromStore(
-            store,
-            widget.children
-                .where((c) => c.isVisible && c.name == null)
-                .map((c) => c.id)),
-        builder: (content, viewModel) {
-          return NavigationBar(
-              labelBehavior: labelBehavior,
-              height: widget.control.attrDouble("height"),
-              elevation: widget.control.attrDouble("elevation"),
-              shadowColor: HexColor.fromString(Theme.of(context),
-                  widget.control.attrString("shadowColor", "")!),
-              surfaceTintColor: HexColor.fromString(Theme.of(context),
-                  widget.control.attrString("surfaceTintColor", "")!),
-              indicatorColor: HexColor.fromString(Theme.of(context),
-                  widget.control.attrString("indicatorColor", "")!),
-              indicatorShape:
-                  parseOutlinedBorder(widget.control, "indicatorShape"),
-              backgroundColor: HexColor.fromString(
-                  Theme.of(context), widget.control.attrString("bgColor", "")!),
-              selectedIndex: _selectedIndex,
-              onDestinationSelected: _destinationChanged,
-              destinations: viewModel.controlViews.map((destView) {
-                var label = destView.control.attrString("label", "")!;
-
-                var icon =
-                    parseIcon(destView.control.attrString("icon", "")!);
-                var iconContentCtrls =
-                    destView.children.where((c) => c.name == "icon_content");
-
-                var selectedIcon = parseIcon(
-                    destView.control.attrString("selectedIcon", "")!);
-                var selectedIconContentCtrls = destView.children
-                    .where((c) => c.name == "selected_icon_content");
-
-                return NavigationDestination(
-                    tooltip: destView.control.attrString("tooltip", "")!,
-                    icon: iconContentCtrls.isNotEmpty
-                        ? createControl(destView.control,
-                            iconContentCtrls.first.id, disabled)
-                        : Icon(icon),
-                    selectedIcon: selectedIconContentCtrls.isNotEmpty
-                        ? createControl(destView.control,
-                            selectedIconContentCtrls.first.id, disabled)
-                        : selectedIcon != null
-                            ? Icon(selectedIcon)
-                            : null,
-                    label: label);
-              }).toList());
-        });
-
-    return constrainedControl(context, navBar, widget.parent, widget.control);
+    return withPagePlatform((context, platform) {
+      bool adaptive = widget.control.attrBool("adaptive", false)!;
+      if (adaptive &&
+          (platform == TargetPlatform.iOS ||
+              platform == TargetPlatform.macOS)) {
+        return CupertinoNavigationBarControl(
+            control: widget.control,
+            children: widget.children,
+            parentDisabled: widget.parentDisabled);
+      }
+
+      bool disabled = widget.control.isDisabled || widget.parentDisabled;
+      var selectedIndex = widget.control.attrInt("selectedIndex", 0)!;
+
+      if (_selectedIndex != selectedIndex) {
+        _selectedIndex = selectedIndex;
+      }
+
+      NavigationDestinationLabelBehavior? labelBehavior =
+          NavigationDestinationLabelBehavior.values.firstWhereOrNull((a) =>
+              a.name.toLowerCase() ==
+              widget.control.attrString("labelBehavior", "")!.toLowerCase());
+
+      var navBar = withControls(
+          widget.children
+              .where((c) => c.isVisible && c.name == null)
+              .map((c) => c.id), (content, viewModel) {
+        return NavigationBar(
+            labelBehavior: labelBehavior,
+            height: widget.control.attrDouble("height"),
+            elevation: widget.control.attrDouble("elevation"),
+            shadowColor: HexColor.fromString(Theme.of(context),
+                widget.control.attrString("shadowColor", "")!),
+            surfaceTintColor: HexColor.fromString(Theme.of(context),
+                widget.control.attrString("surfaceTintColor", "")!),
+            indicatorColor: HexColor.fromString(Theme.of(context),
+                widget.control.attrString("indicatorColor", "")!),
+            indicatorShape:
+                parseOutlinedBorder(widget.control, "indicatorShape"),
+            backgroundColor: HexColor.fromString(
+                Theme.of(context), widget.control.attrString("bgColor", "")!),
+            selectedIndex: _selectedIndex,
+            onDestinationSelected: _destinationChanged,
+            destinations: viewModel.controlViews.map((destView) {
+              var label = destView.control.attrString("label", "")!;
+
+              var icon = parseIcon(destView.control.attrString("icon", "")!);
+              var iconContentCtrls =
+                  destView.children.where((c) => c.name == "icon_content");
+
+              var selectedIcon =
+                  parseIcon(destView.control.attrString("selectedIcon", "")!);
+              var selectedIconContentCtrls = destView.children
+                  .where((c) => c.name == "selected_icon_content");
+
+              return NavigationDestination(
+                  tooltip: destView.control.attrString("tooltip", "")!,
+                  icon: iconContentCtrls.isNotEmpty
+                      ? createControl(
+                          destView.control, iconContentCtrls.first.id, disabled)
+                      : Icon(icon),
+                  selectedIcon: selectedIconContentCtrls.isNotEmpty
+                      ? createControl(destView.control,
+                          selectedIconContentCtrls.first.id, disabled)
+                      : selectedIcon != null
+                          ? Icon(selectedIcon)
+                          : null,
+                  label: label);
+            }).toList());
+      });
+
+      return constrainedControl(context, navBar, widget.parent, widget.control);
+    });
   }
 }
diff --git a/package/lib/src/controls/navigation_drawer.dart b/package/lib/src/controls/navigation_drawer.dart
index 5960894ff..569cce3ce 100644
--- a/package/lib/src/controls/navigation_drawer.dart
+++ b/package/lib/src/controls/navigation_drawer.dart
@@ -1,55 +1,42 @@
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/controls_view_model.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/borders.dart';
 import '../utils/colors.dart';
 import '../utils/edge_insets.dart';
 import '../utils/icons.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
 
 class NavigationDrawerControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final List<Control> children;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const NavigationDrawerControl(
       {super.key,
       this.parent,
       required this.control,
       required this.children,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<NavigationDrawerControl> createState() =>
       _NavigationDrawerControlState();
 }
 
-class _NavigationDrawerControlState extends State<NavigationDrawerControl> {
+class _NavigationDrawerControlState extends State<NavigationDrawerControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   int _selectedIndex = 0;
 
   void _destinationChanged(int index) {
     _selectedIndex = index;
     debugPrint("Selected index: $_selectedIndex");
-    List<Map<String, String>> props = [
-      {"i": widget.control.id, "selectedindex": _selectedIndex.toString()}
-    ];
-    widget.dispatch(
-        UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-    final server = FletAppServices.of(context).server;
-    server.updateControlProps(props: props);
-    server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: "change",
-        eventData: _selectedIndex.toString());
+    updateControlProps(
+        widget.control.id, {"selectedindex": _selectedIndex.toString()});
+    sendControlEvent(widget.control.id, "change", _selectedIndex.toString());
   }
 
   @override
@@ -63,64 +50,57 @@ class _NavigationDrawerControlState extends State<NavigationDrawerControl> {
       _selectedIndex = selectedIndex;
     }
 
-    var navDrawer = StoreConnector<AppState, ControlsViewModel>(
-        distinct: true,
-        converter: (store) => ControlsViewModel.fromStore(
-            store,
-            widget.children
-                .where((c) => c.isVisible && c.name == null)
-                .map((c) => c.id)),
-        builder: (content, viewModel) {
-          List<Widget> children = viewModel.controlViews.map((destView) {
-            if (destView.control.type == "navigationdrawerdestination") {
-              var icon =
-                  parseIcon(destView.control.attrString("icon", "")!);
-              var iconContentCtrls =
-                  destView.children.where((c) => c.name == "icon_content");
-              var selectedIcon = parseIcon(
-                  destView.control.attrString("selectedIcon", "")!);
-              var selectedIconContentCtrls = destView.children
-                  .where((c) => c.name == "selected_icon_content");
-              return NavigationDrawerDestination(
-                // backgroundColor: HexColor.fromString(Theme.of(context),
-                //     destView.control.attrString("bgColor", "")!),
-                // flutter issue https://github.com/flutter/flutter/issues/138105
-                icon: iconContentCtrls.isNotEmpty
-                    ? createControl(
-                        destView.control, iconContentCtrls.first.id, disabled)
-                    : Icon(icon),
-                label: Text(destView.control.attrString("label", "")!),
-                selectedIcon: selectedIconContentCtrls.isNotEmpty
-                    ? createControl(destView.control,
-                        selectedIconContentCtrls.first.id, disabled)
-                    : selectedIcon != null
-                        ? Icon(selectedIcon)
-                        : null,
-              );
-            } else {
-              return createControl(
-                  widget.control, destView.control.id, disabled);
-            }
-          }).toList();
-          return NavigationDrawer(
-            elevation: widget.control.attrDouble("elevation"),
-            indicatorColor: HexColor.fromString(Theme.of(context),
-                widget.control.attrString("indicatorColor", "")!),
-            indicatorShape:
-                parseOutlinedBorder(widget.control, "indicatorShape"),
-            backgroundColor: HexColor.fromString(
-                Theme.of(context), widget.control.attrString("bgColor", "")!),
-            selectedIndex: _selectedIndex,
-            shadowColor: HexColor.fromString(Theme.of(context),
-                widget.control.attrString("shadowColor", "")!),
-            surfaceTintColor: HexColor.fromString(Theme.of(context),
-                widget.control.attrString("surfaceTintColor", "")!),
-            tilePadding: parseEdgeInsets(widget.control, "tilePadding") ??
-                const EdgeInsets.symmetric(horizontal: 12.0),
-            onDestinationSelected: _destinationChanged,
-            children: children,
+    var navDrawer = withControls(
+        widget.children
+            .where((c) => c.isVisible && c.name == null)
+            .map((c) => c.id), (content, viewModel) {
+      List<Widget> children = viewModel.controlViews.map((destView) {
+        if (destView.control.type == "navigationdrawerdestination") {
+          var icon = parseIcon(destView.control.attrString("icon", "")!);
+          var iconContentCtrls =
+              destView.children.where((c) => c.name == "icon_content");
+          var selectedIcon =
+              parseIcon(destView.control.attrString("selectedIcon", "")!);
+          var selectedIconContentCtrls =
+              destView.children.where((c) => c.name == "selected_icon_content");
+          return NavigationDrawerDestination(
+            // backgroundColor: HexColor.fromString(Theme.of(context),
+            //     destView.control.attrString("bgColor", "")!),
+            // flutter issue https://github.com/flutter/flutter/issues/138105
+            icon: iconContentCtrls.isNotEmpty
+                ? createControl(
+                    destView.control, iconContentCtrls.first.id, disabled)
+                : Icon(icon),
+            label: Text(destView.control.attrString("label", "")!),
+            selectedIcon: selectedIconContentCtrls.isNotEmpty
+                ? createControl(destView.control,
+                    selectedIconContentCtrls.first.id, disabled)
+                : selectedIcon != null
+                    ? Icon(selectedIcon)
+                    : null,
           );
-        });
+        } else {
+          return createControl(widget.control, destView.control.id, disabled);
+        }
+      }).toList();
+      return NavigationDrawer(
+        elevation: widget.control.attrDouble("elevation"),
+        indicatorColor: HexColor.fromString(Theme.of(context),
+            widget.control.attrString("indicatorColor", "")!),
+        indicatorShape: parseOutlinedBorder(widget.control, "indicatorShape"),
+        backgroundColor: HexColor.fromString(
+            Theme.of(context), widget.control.attrString("bgColor", "")!),
+        selectedIndex: _selectedIndex,
+        shadowColor: HexColor.fromString(
+            Theme.of(context), widget.control.attrString("shadowColor", "")!),
+        surfaceTintColor: HexColor.fromString(Theme.of(context),
+            widget.control.attrString("surfaceTintColor", "")!),
+        tilePadding: parseEdgeInsets(widget.control, "tilePadding") ??
+            const EdgeInsets.symmetric(horizontal: 12.0),
+        onDestinationSelected: _destinationChanged,
+        children: children,
+      );
+    });
 
     return navDrawer;
   }
diff --git a/package/lib/src/controls/navigation_rail.dart b/package/lib/src/controls/navigation_rail.dart
index b3d11d32f..5aec8e984 100644
--- a/package/lib/src/controls/navigation_rail.dart
+++ b/package/lib/src/controls/navigation_rail.dart
@@ -1,55 +1,42 @@
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/controls_view_model.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/borders.dart';
 import '../utils/colors.dart';
 import '../utils/edge_insets.dart';
 import '../utils/icons.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
 
 class NavigationRailControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final List<Control> children;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const NavigationRailControl(
       {super.key,
       this.parent,
       required this.control,
       required this.children,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<NavigationRailControl> createState() => _NavigationRailControlState();
 }
 
-class _NavigationRailControlState extends State<NavigationRailControl> {
+class _NavigationRailControlState extends State<NavigationRailControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   int? _selectedIndex;
 
   void _destinationChanged(int index) {
     _selectedIndex = index;
     debugPrint("Selected index: $_selectedIndex");
-    List<Map<String, String>> props = [
-      {"i": widget.control.id, "selectedindex": _selectedIndex.toString()}
-    ];
-    widget.dispatch(
-        UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-    final server = FletAppServices.of(context).server;
-    server.updateControlProps(props: props);
-    server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: "change",
-        eventData: _selectedIndex.toString());
+    updateControlProps(
+        widget.control.id, {"selectedindex": _selectedIndex.toString()});
+    sendControlEvent(widget.control.id, "change", _selectedIndex.toString());
   }
 
   @override
@@ -77,87 +64,81 @@ class _NavigationRailControlState extends State<NavigationRailControl> {
 
     var extended = widget.control.attrBool("extended", false)!;
 
-    var rail = StoreConnector<AppState, ControlsViewModel>(
-        distinct: true,
-        converter: (store) => ControlsViewModel.fromStore(
-            store,
-            widget.children
-                .where((c) => c.isVisible && c.name == null)
-                .map((c) => c.id)),
-        builder: (content, viewModel) {
-          return LayoutBuilder(
-            builder: (BuildContext context, BoxConstraints constraints) {
-              debugPrint(
-                  "NavigationRail constraints.maxWidth: ${constraints.maxWidth}");
-              debugPrint(
-                  "NavigationRail constraints.maxHeight: ${constraints.maxHeight}");
-
-              if (constraints.maxHeight == double.infinity &&
-                  widget.control.attrs["height"] == null) {
-                return const ErrorControl("Error displaying NavigationRail",
-                    description:
-                        "Control's height is unbounded. Either set \"expand\" property, set a fixed \"height\" or nest NavigationRail inside another control with a fixed height.");
-              }
-
-              return NavigationRail(
-                  labelType:
-                      extended ? NavigationRailLabelType.none : labelType,
-                  extended: extended,
-                  elevation: widget.control.attrDouble("elevation", 0),
-                  indicatorShape: parseOutlinedBorder(widget.control, "indicatorShape"),
-                  minWidth: widget.control.attrDouble("minWidth"),
-                  minExtendedWidth:
-                      widget.control.attrDouble("minExtendedWidth"),
-                  groupAlignment: widget.control.attrDouble("groupAlignment"),
-                  backgroundColor: HexColor.fromString(Theme.of(context),
-                      widget.control.attrString("bgColor", "")!),
-                  indicatorColor: HexColor.fromString(Theme.of(context),
-                      widget.control.attrString("indicatorColor", "")!),
-                  leading: leadingCtrls.isNotEmpty
-                      ? createControl(
-                          widget.control, leadingCtrls.first.id, disabled)
-                      : null,
-                  trailing: trailingCtrls.isNotEmpty
-                      ? createControl(
-                          widget.control, trailingCtrls.first.id, disabled)
-                      : null,
-                  selectedIndex: _selectedIndex,
-                  onDestinationSelected: _destinationChanged,
-                  destinations: viewModel.controlViews.map((destView) {
-                    var label = destView.control.attrString("label", "")!;
-                    var labelContentCtrls = destView.children
-                        .where((c) => c.name == "label_content");
-
-                    var icon = parseIcon(
-                        destView.control.attrString("icon", "")!);
-                    var iconContentCtrls = destView.children
-                        .where((c) => c.name == "icon_content");
-
-                    var selectedIcon = parseIcon(
-                        destView.control.attrString("selectedIcon", "")!);
-                    var selectedIconContentCtrls = destView.children
-                        .where((c) => c.name == "selected_icon_content");
-
-                    return NavigationRailDestination(
-                        padding: parseEdgeInsets(destView.control, "padding"),
-                        icon: iconContentCtrls.isNotEmpty
-                            ? createControl(destView.control,
-                                iconContentCtrls.first.id, disabled)
-                            : Icon(icon),
-                        selectedIcon: selectedIconContentCtrls.isNotEmpty
-                            ? createControl(destView.control,
-                                selectedIconContentCtrls.first.id, disabled)
-                            : selectedIcon != null
-                                ? Icon(selectedIcon)
-                                : null,
-                        label: labelContentCtrls.isNotEmpty
-                            ? createControl(destView.control,
-                                labelContentCtrls.first.id, disabled)
-                            : Text(label));
-                  }).toList());
-            },
-          );
-        });
+    var rail = withControls(
+        widget.children
+            .where((c) => c.isVisible && c.name == null)
+            .map((c) => c.id), (content, viewModel) {
+      return LayoutBuilder(
+        builder: (BuildContext context, BoxConstraints constraints) {
+          debugPrint(
+              "NavigationRail constraints.maxWidth: ${constraints.maxWidth}");
+          debugPrint(
+              "NavigationRail constraints.maxHeight: ${constraints.maxHeight}");
+
+          if (constraints.maxHeight == double.infinity &&
+              widget.control.attrs["height"] == null) {
+            return const ErrorControl("Error displaying NavigationRail",
+                description:
+                    "Control's height is unbounded. Either set \"expand\" property, set a fixed \"height\" or nest NavigationRail inside another control with a fixed height.");
+          }
+
+          return NavigationRail(
+              labelType: extended ? NavigationRailLabelType.none : labelType,
+              extended: extended,
+              elevation: widget.control.attrDouble("elevation", 0),
+              indicatorShape:
+                  parseOutlinedBorder(widget.control, "indicatorShape"),
+              minWidth: widget.control.attrDouble("minWidth"),
+              minExtendedWidth: widget.control.attrDouble("minExtendedWidth"),
+              groupAlignment: widget.control.attrDouble("groupAlignment"),
+              backgroundColor: HexColor.fromString(
+                  Theme.of(context), widget.control.attrString("bgColor", "")!),
+              indicatorColor: HexColor.fromString(Theme.of(context),
+                  widget.control.attrString("indicatorColor", "")!),
+              leading: leadingCtrls.isNotEmpty
+                  ? createControl(
+                      widget.control, leadingCtrls.first.id, disabled)
+                  : null,
+              trailing: trailingCtrls.isNotEmpty
+                  ? createControl(
+                      widget.control, trailingCtrls.first.id, disabled)
+                  : null,
+              selectedIndex: _selectedIndex,
+              onDestinationSelected: _destinationChanged,
+              destinations: viewModel.controlViews.map((destView) {
+                var label = destView.control.attrString("label", "")!;
+                var labelContentCtrls =
+                    destView.children.where((c) => c.name == "label_content");
+
+                var icon = parseIcon(destView.control.attrString("icon", "")!);
+                var iconContentCtrls =
+                    destView.children.where((c) => c.name == "icon_content");
+
+                var selectedIcon =
+                    parseIcon(destView.control.attrString("selectedIcon", "")!);
+                var selectedIconContentCtrls = destView.children
+                    .where((c) => c.name == "selected_icon_content");
+
+                return NavigationRailDestination(
+                    padding: parseEdgeInsets(destView.control, "padding"),
+                    icon: iconContentCtrls.isNotEmpty
+                        ? createControl(destView.control,
+                            iconContentCtrls.first.id, disabled)
+                        : Icon(icon),
+                    selectedIcon: selectedIconContentCtrls.isNotEmpty
+                        ? createControl(destView.control,
+                            selectedIconContentCtrls.first.id, disabled)
+                        : selectedIcon != null
+                            ? Icon(selectedIcon)
+                            : null,
+                    label: labelContentCtrls.isNotEmpty
+                        ? createControl(destView.control,
+                            labelContentCtrls.first.id, disabled)
+                        : Text(label));
+              }).toList());
+        },
+      );
+    });
 
     return constrainedControl(context, rail, widget.parent, widget.control);
   }
diff --git a/package/lib/src/controls/outlined_button.dart b/package/lib/src/controls/outlined_button.dart
index 65dfd30af..f590c6dec 100644
--- a/package/lib/src/controls/outlined_button.dart
+++ b/package/lib/src/controls/outlined_button.dart
@@ -1,12 +1,12 @@
 import 'package:flutter/material.dart';
 
-import '../flet_app_services.dart';
 import '../models/control.dart';
 import '../utils/buttons.dart';
 import '../utils/colors.dart';
 import '../utils/icons.dart';
 import '../utils/launch_url.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class OutlinedButtonControl extends StatefulWidget {
   final Control? parent;
@@ -25,7 +25,8 @@ class OutlinedButtonControl extends StatefulWidget {
   State<OutlinedButtonControl> createState() => _OutlinedButtonControlState();
 }
 
-class _OutlinedButtonControlState extends State<OutlinedButtonControl> {
+class _OutlinedButtonControlState extends State<OutlinedButtonControl>
+    with FletControlStatefulMixin {
   late final FocusNode _focusNode;
   String? _lastFocusValue;
 
@@ -44,18 +45,14 @@ class _OutlinedButtonControlState extends State<OutlinedButtonControl> {
   }
 
   void _onFocusChange() {
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   @override
   Widget build(BuildContext context) {
     debugPrint("Button build: ${widget.control.id}");
 
-    final server = FletAppServices.of(context).server;
-
     String text = widget.control.attrString("text", "")!;
     IconData? icon = parseIcon(widget.control.attrString("icon", "")!);
     Color? iconColor = HexColor.fromString(
@@ -74,30 +71,21 @@ class _OutlinedButtonControlState extends State<OutlinedButtonControl> {
             if (url != "") {
               openWebBrowser(url, webWindowName: urlTarget);
             }
-            server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "click",
-                eventData: "");
+            sendControlEvent(widget.control.id, "click", "");
           }
         : null;
 
     Function()? onLongPressHandler = onLongPress && !disabled
         ? () {
             debugPrint("Button ${widget.control.id} long pressed!");
-            server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "long_press",
-                eventData: "");
+            sendControlEvent(widget.control.id, "long_press", "");
           }
         : null;
 
     Function(bool)? onHoverHandler = onHover && !disabled
         ? (state) {
             debugPrint("Button ${widget.control.id} hovered!");
-            server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "hover",
-                eventData: state.toString());
+            sendControlEvent(widget.control.id, "hover", state.toString());
           }
         : null;
 
diff --git a/package/lib/src/controls/page.dart b/package/lib/src/controls/page.dart
index dfac6cc65..1220c7b8e 100644
--- a/package/lib/src/controls/page.dart
+++ b/package/lib/src/controls/page.dart
@@ -1,25 +1,21 @@
 import 'dart:convert';
 
 import 'package:collection/collection.dart';
-import 'package:flet/src/controls/floating_action_button.dart';
-import 'package:flet/src/flet_app_context.dart';
+import 'package:equatable/equatable.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_redux/flutter_redux.dart';
+import 'package:redux/redux.dart';
 
 import '../actions.dart';
+import '../flet_app_context.dart';
 import '../flet_app_services.dart';
 import '../models/app_state.dart';
 import '../models/control.dart';
 import '../models/control_view_model.dart';
-import '../models/controls_view_model.dart';
-import '../models/page_args_model.dart';
 import '../models/page_media_view_model.dart';
-import '../models/routes_view_model.dart';
-import '../protocol/keyboard_event.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../routing/route_parser.dart';
 import '../routing/route_state.dart';
 import '../routing/router_delegate.dart';
@@ -37,10 +33,75 @@ import '../widgets/window_media.dart';
 import 'app_bar.dart';
 import 'create_control.dart';
 import 'cupertino_app_bar.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
+import 'floating_action_button.dart';
 import 'navigation_drawer.dart';
 import 'scroll_notification_control.dart';
 import 'scrollable_control.dart';
 
+class RoutesViewModel extends Equatable {
+  final Control page;
+  final bool isLoading;
+  final String error;
+  final List<Control> offstageControls;
+  final List<Control> views;
+
+  const RoutesViewModel(
+      {required this.page,
+      required this.isLoading,
+      required this.error,
+      required this.offstageControls,
+      required this.views});
+
+  static RoutesViewModel fromStore(Store<AppState> store) {
+    Control? offstageControl = store.state.controls["page"]!.childIds
+        .map((childId) => store.state.controls[childId]!)
+        .firstWhereOrNull((c) => c.type == "offstage");
+
+    return RoutesViewModel(
+        page: store.state.controls["page"]!,
+        isLoading: store.state.isLoading,
+        error: store.state.error,
+        offstageControls: offstageControl != null
+            ? store.state.controls[offstageControl.id]!.childIds
+                .map((childId) => store.state.controls[childId]!)
+                .where((c) => c.isVisible)
+                .toList()
+            : [],
+        views: store.state.controls["page"]!.childIds
+            .map((childId) => store.state.controls[childId]!)
+            .where((c) => c.type != "offstage" && c.isVisible)
+            .toList());
+  }
+
+  @override
+  List<Object?> get props => [page, isLoading, error, offstageControls, views];
+}
+
+class KeyboardEvent {
+  final String key;
+  final bool isShiftPressed;
+  final bool isControlPressed;
+  final bool isAltPressed;
+  final bool isMetaPressed;
+
+  KeyboardEvent(
+      {required this.key,
+      required this.isShiftPressed,
+      required this.isControlPressed,
+      required this.isAltPressed,
+      required this.isMetaPressed});
+
+  Map<String, dynamic> toJson() => <String, dynamic>{
+        'key': key,
+        'shift': isShiftPressed,
+        'ctrl': isControlPressed,
+        'alt': isAltPressed,
+        'meta': isMetaPressed
+      };
+}
+
 class PageControl extends StatefulWidget {
   final Control? parent;
   final Control control;
@@ -58,7 +119,8 @@ class PageControl extends StatefulWidget {
   State<PageControl> createState() => _PageControlState();
 }
 
-class _PageControlState extends State<PageControl> {
+class _PageControlState extends State<PageControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   String? _windowTitle;
   Color? _windowBgcolor;
   double? _windowWidth;
@@ -141,10 +203,10 @@ class _PageControlState extends State<PageControl> {
         LogicalKeyboardKey.shiftLeft,
         LogicalKeyboardKey.shiftRight
       ].contains(k)) {
-        FletAppServices.of(context).server.sendPageEvent(
-            eventTarget: "page",
-            eventName: "keyboard_event",
-            eventData: json.encode(KeyboardEvent(
+        sendControlEvent(
+            "page",
+            "keyboard_event",
+            json.encode(KeyboardEvent(
                     key: k.keyLabel,
                     isAltPressed: e.isAltPressed,
                     isControlPressed: e.isControlPressed,
@@ -424,45 +486,42 @@ class _PageControlState extends State<PageControl> {
 
     updateWindow();
 
-    return StoreConnector<AppState, PageArgsModel>(
-        distinct: true,
-        converter: (store) => PageArgsModel.fromStore(store),
-        builder: (context, pageArgs) {
-          debugPrint("Page fonts build: ${widget.control.id}");
-
-          // load custom fonts
-          parseFonts(widget.control, "fonts").forEach((fontFamily, fontUrl) {
-            var assetSrc =
-                getAssetSrc(fontUrl, pageArgs.pageUri!, pageArgs.assetsDir);
-
-            if (assetSrc.isFile) {
-              UserFonts.loadFontFromFile(fontFamily, assetSrc.path);
-            } else {
-              UserFonts.loadFontFromUrl(fontFamily, assetSrc.path);
-            }
-          });
+    return withPageArgs((context, pageArgs) {
+      debugPrint("Page fonts build: ${widget.control.id}");
 
-          return StoreConnector<AppState, PageMediaViewModel>(
-              distinct: true,
-              converter: (store) => PageMediaViewModel.fromStore(store),
-              builder: (context, media) {
-                debugPrint("MaterialApp.router build: ${widget.control.id}");
-
-                return FletAppContext(
-                    themeMode: themeMode,
-                    child: MaterialApp.router(
-                      debugShowCheckedModeBanner: false,
-                      showSemanticsDebugger: widget.control
-                          .attrBool("showSemanticsDebugger", false)!,
-                      routerDelegate: _routerDelegate,
-                      routeInformationParser: _routeParser,
-                      title: windowTitle,
-                      theme: theme,
-                      darkTheme: darkTheme,
-                      themeMode: themeMode,
-                    ));
-              });
-        });
+      // load custom fonts
+      parseFonts(widget.control, "fonts").forEach((fontFamily, fontUrl) {
+        var assetSrc =
+            getAssetSrc(fontUrl, pageArgs.pageUri!, pageArgs.assetsDir);
+
+        if (assetSrc.isFile) {
+          UserFonts.loadFontFromFile(fontFamily, assetSrc.path);
+        } else {
+          UserFonts.loadFontFromUrl(fontFamily, assetSrc.path);
+        }
+      });
+
+      return StoreConnector<AppState, PageMediaViewModel>(
+          distinct: true,
+          converter: (store) => PageMediaViewModel.fromStore(store),
+          builder: (context, media) {
+            debugPrint("MaterialApp.router build: ${widget.control.id}");
+
+            return FletAppContext(
+                themeMode: themeMode,
+                child: MaterialApp.router(
+                  debugShowCheckedModeBanner: false,
+                  showSemanticsDebugger:
+                      widget.control.attrBool("showSemanticsDebugger", false)!,
+                  routerDelegate: _routerDelegate,
+                  routeInformationParser: _routeParser,
+                  title: windowTitle,
+                  theme: theme,
+                  darkTheme: darkTheme,
+                  themeMode: themeMode,
+                ));
+          });
+    });
   }
 
   Widget _buildNavigator(
@@ -512,7 +571,7 @@ class _PageControlState extends State<PageControl> {
               }
 
               if (viewId == routesView.views.first.id && isDesktop()) {
-                overlayWidgets.add(const WindowMedia());
+                overlayWidgets.add(WindowMedia(dispatch: widget.dispatch));
               }
 
               return overlayWidgets;
@@ -536,8 +595,7 @@ class _PageControlState extends State<PageControl> {
                   parent: routesView.page,
                   viewId: view.id,
                   overlayWidgets: overlayWidgets(view.id),
-                  loadingPage: loadingPage,
-                  dispatch: widget.dispatch);
+                  loadingPage: loadingPage);
 
               //debugPrint("ROUTES: $_prevViewRoutes $viewRoutes");
 
@@ -562,11 +620,8 @@ class _PageControlState extends State<PageControl> {
               key: navigatorKey,
               pages: pages,
               onPopPage: (route, dynamic result) {
-                FletAppServices.of(context).server.sendPageEvent(
-                    eventTarget: "page",
-                    eventName: "view_pop",
-                    eventData:
-                        ((route.settings as Page).key as ValueKey).value);
+                sendControlEvent("page", "view_pop",
+                    ((route.settings as Page).key as ValueKey).value);
                 return false;
               });
 
@@ -588,21 +643,20 @@ class ViewControl extends StatefulWidget {
   final String viewId;
   final List<Widget> overlayWidgets;
   final Widget? loadingPage;
-  final dynamic dispatch;
 
   const ViewControl(
       {super.key,
       required this.parent,
       required this.viewId,
       required this.overlayWidgets,
-      required this.loadingPage,
-      required this.dispatch});
+      required this.loadingPage});
 
   @override
   State<ViewControl> createState() => _ViewControlState();
 }
 
-class _ViewControlState extends State<ViewControl> {
+class _ViewControlState extends State<ViewControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   final scaffoldKey = GlobalKey<ScaffoldState>();
 
   @override
@@ -619,7 +673,7 @@ class _ViewControlState extends State<ViewControl> {
         //   debugPrint("View StoreConnector.onWillChange(): $prev, $next");
         // },
         builder: (context, controlView) {
-          debugPrint("View StoreConnector");
+          debugPrint("View build");
 
           if (controlView == null) {
             return const SizedBox.shrink();
@@ -698,195 +752,167 @@ class _ViewControlState extends State<ViewControl> {
               ? TextDirection.rtl
               : TextDirection.ltr;
 
-          return StoreConnector<AppState, ControlsViewModel>(
-              distinct: true,
-              converter: (store) =>
-                  ControlsViewModel.fromStore(store, childIds),
-              ignoreChange: (state) {
-                //debugPrint("ignoreChange: $id");
-                for (var id in childIds) {
-                  if (state.controls[id] == null) {
-                    return true;
-                  }
-                }
-                return false;
-              },
-              builder: (context, childrenViews) {
-                debugPrint("Route view StoreConnector build: ${widget.viewId}");
-
-                var appBarView = childrenViews.controlViews.firstWhereOrNull(
-                    (v) => v.control.id == (appBar?.id ?? ""));
-                var cupertinoAppBarView = childrenViews.controlViews
-                    .firstWhereOrNull(
-                        (v) => v.control.id == (cupertinoAppBar?.id ?? ""));
-                var drawerView = childrenViews.controlViews.firstWhereOrNull(
-                    (v) => v.control.id == (drawer?.id ?? ""));
-                var endDrawerView = childrenViews.controlViews.firstWhereOrNull(
-                    (v) => v.control.id == (endDrawer?.id ?? ""));
-
-                var column = Column(
-                    mainAxisAlignment: mainAlignment,
-                    crossAxisAlignment: crossAlignment,
-                    children: controls);
-
-                Widget child = ScrollableControl(
-                  control: control,
-                  scrollDirection: Axis.vertical,
-                  dispatch: widget.dispatch,
-                  child: column,
-                );
-
-                if (control.attrBool("onScroll", false)!) {
-                  child =
-                      ScrollNotificationControl(control: control, child: child);
-                }
+          return withControls(childIds, (context, childrenViews) {
+            debugPrint("Route view build: ${widget.viewId}");
+
+            var appBarView = childrenViews.controlViews
+                .firstWhereOrNull((v) => v.control.id == (appBar?.id ?? ""));
+            var cupertinoAppBarView = childrenViews.controlViews
+                .firstWhereOrNull(
+                    (v) => v.control.id == (cupertinoAppBar?.id ?? ""));
+            var drawerView = childrenViews.controlViews
+                .firstWhereOrNull((v) => v.control.id == (drawer?.id ?? ""));
+            var endDrawerView = childrenViews.controlViews
+                .firstWhereOrNull((v) => v.control.id == (endDrawer?.id ?? ""));
+
+            var column = Column(
+                mainAxisAlignment: mainAlignment,
+                crossAxisAlignment: crossAlignment,
+                children: controls);
+
+            Widget child = ScrollableControl(
+              control: control,
+              scrollDirection: Axis.vertical,
+              child: column,
+            );
+
+            if (control.attrBool("onScroll", false)!) {
+              child = ScrollNotificationControl(control: control, child: child);
+            }
 
-                final bool? drawerOpened = widget.parent.state["drawerOpened"];
-                final bool? endDrawerOpened =
-                    widget.parent.state["endDrawerOpened"];
-
-                void dismissDrawer(String id) {
-                  List<Map<String, String>> props = [
-                    {"i": id, "open": "false"}
-                  ];
-                  widget.dispatch(UpdateControlPropsAction(
-                      UpdateControlPropsPayload(props: props)));
-                  FletAppServices.of(context)
-                      .server
-                      .updateControlProps(props: props);
-                  FletAppServices.of(context).server.sendPageEvent(
-                      eventTarget: id, eventName: "dismiss", eventData: "");
-                }
+            final bool? drawerOpened = widget.parent.state["drawerOpened"];
+            final bool? endDrawerOpened =
+                widget.parent.state["endDrawerOpened"];
+
+            void dismissDrawer(String id) {
+              updateControlProps(id, {"open": "false"});
+              sendControlEvent(id, "dismiss", "");
+            }
 
-                WidgetsBinding.instance.addPostFrameCallback((_) {
-                  if (drawerView != null) {
-                    if (scaffoldKey.currentState?.isDrawerOpen == false &&
-                        drawerOpened == true) {
-                      widget.parent.state["drawerOpened"] = false;
-                      dismissDrawer(drawerView.control.id);
-                    }
-                    if (drawerView.control.attrBool("open", false)! &&
-                        drawerOpened != true) {
-                      if (scaffoldKey.currentState?.isEndDrawerOpen == true) {
-                        scaffoldKey.currentState?.closeEndDrawer();
-                      }
-                      Future.delayed(const Duration(milliseconds: 1))
-                          .then((value) {
-                        scaffoldKey.currentState?.openDrawer();
-                        widget.parent.state["drawerOpened"] = true;
-                      });
-                    } else if (!drawerView.control.attrBool("open", false)! &&
-                        drawerOpened == true) {
-                      scaffoldKey.currentState?.closeDrawer();
-                      widget.parent.state["drawerOpened"] = false;
-                    }
+            WidgetsBinding.instance.addPostFrameCallback((_) {
+              if (drawerView != null) {
+                if (scaffoldKey.currentState?.isDrawerOpen == false &&
+                    drawerOpened == true) {
+                  widget.parent.state["drawerOpened"] = false;
+                  dismissDrawer(drawerView.control.id);
+                }
+                if (drawerView.control.attrBool("open", false)! &&
+                    drawerOpened != true) {
+                  if (scaffoldKey.currentState?.isEndDrawerOpen == true) {
+                    scaffoldKey.currentState?.closeEndDrawer();
                   }
-                  if (endDrawerView != null) {
-                    if (scaffoldKey.currentState?.isEndDrawerOpen == false &&
-                        endDrawerOpened == true) {
-                      widget.parent.state["endDrawerOpened"] = false;
-                      dismissDrawer(endDrawerView.control.id);
-                    }
-                    if (endDrawerView.control.attrBool("open", false)! &&
-                        endDrawerOpened != true) {
-                      if (scaffoldKey.currentState?.isDrawerOpen == true) {
-                        scaffoldKey.currentState?.closeDrawer();
-                      }
-                      Future.delayed(const Duration(milliseconds: 1))
-                          .then((value) {
-                        scaffoldKey.currentState?.openEndDrawer();
-                        widget.parent.state["endDrawerOpened"] = true;
-                      });
-                    } else if (!endDrawerView.control
-                            .attrBool("open", false)! &&
-                        endDrawerOpened == true) {
-                      scaffoldKey.currentState?.closeEndDrawer();
-                      widget.parent.state["endDrawerOpened"] = false;
-                    }
+                  Future.delayed(const Duration(milliseconds: 1)).then((value) {
+                    scaffoldKey.currentState?.openDrawer();
+                    widget.parent.state["drawerOpened"] = true;
+                  });
+                } else if (!drawerView.control.attrBool("open", false)! &&
+                    drawerOpened == true) {
+                  scaffoldKey.currentState?.closeDrawer();
+                  widget.parent.state["drawerOpened"] = false;
+                }
+              }
+              if (endDrawerView != null) {
+                if (scaffoldKey.currentState?.isEndDrawerOpen == false &&
+                    endDrawerOpened == true) {
+                  widget.parent.state["endDrawerOpened"] = false;
+                  dismissDrawer(endDrawerView.control.id);
+                }
+                if (endDrawerView.control.attrBool("open", false)! &&
+                    endDrawerOpened != true) {
+                  if (scaffoldKey.currentState?.isDrawerOpen == true) {
+                    scaffoldKey.currentState?.closeDrawer();
                   }
-                });
-
-                var bnb = navBar ?? bottomAppBar;
-
-                var bar = appBarView != null
-                    ? AppBarControl(
+                  Future.delayed(const Duration(milliseconds: 1)).then((value) {
+                    scaffoldKey.currentState?.openEndDrawer();
+                    widget.parent.state["endDrawerOpened"] = true;
+                  });
+                } else if (!endDrawerView.control.attrBool("open", false)! &&
+                    endDrawerOpened == true) {
+                  scaffoldKey.currentState?.closeEndDrawer();
+                  widget.parent.state["endDrawerOpened"] = false;
+                }
+              }
+            });
+
+            var bnb = navBar ?? bottomAppBar;
+
+            var bar = appBarView != null
+                ? AppBarControl(
+                    parent: control,
+                    control: appBarView.control,
+                    children: appBarView.children,
+                    parentDisabled: control.isDisabled,
+                    height: appBarView.control
+                        .attrDouble("toolbarHeight", kToolbarHeight)!)
+                : cupertinoAppBarView != null
+                    ? CupertinoAppBarControl(
                         parent: control,
-                        control: appBarView.control,
-                        children: appBarView.children,
+                        control: cupertinoAppBarView.control,
+                        children: cupertinoAppBarView.children,
                         parentDisabled: control.isDisabled,
-                        height: appBarView.control
-                            .attrDouble("toolbarHeight", kToolbarHeight)!)
-                    : cupertinoAppBarView != null
-                        ? CupertinoAppBarControl(
-                            parent: control,
-                            control: cupertinoAppBarView.control,
-                            children: cupertinoAppBarView.children,
-                            parentDisabled: control.isDisabled,
-                            bgcolor: HexColor.fromString(
-                                Theme.of(context),
-                                cupertinoAppBarView.control
-                                    .attrString("bgcolor", "")!),
-                          ) as ObstructingPreferredSizeWidget
-                        : null;
-
-                var scaffold = Scaffold(
-                  key: scaffoldKey,
-                  backgroundColor: HexColor.fromString(
-                      Theme.of(context), control.attrString("bgcolor", "")!),
-                  appBar: bar,
-                  drawer: drawerView != null
-                      ? NavigationDrawerControl(
-                          control: drawerView.control,
-                          children: drawerView.children,
-                          parentDisabled: control.isDisabled,
-                          dispatch: widget.dispatch,
-                        )
-                      : null,
-                  onDrawerChanged: (opened) {
-                    if (drawerView != null && !opened) {
-                      widget.parent.state["drawerOpened"] = false;
-                      dismissDrawer(drawerView.control.id);
-                    }
-                  },
-                  endDrawer: endDrawerView != null
-                      ? NavigationDrawerControl(
-                          control: endDrawerView.control,
-                          children: endDrawerView.children,
-                          parentDisabled: control.isDisabled,
-                          dispatch: widget.dispatch,
-                        )
-                      : null,
-                  onEndDrawerChanged: (opened) {
-                    if (endDrawerView != null && !opened) {
-                      widget.parent.state["endDrawerOpened"] = false;
-                      dismissDrawer(endDrawerView.control.id);
-                    }
-                  },
-                  body: Stack(children: [
-                    SizedBox.expand(
-                        child: Container(
-                            padding: parseEdgeInsets(control, "padding") ??
-                                const EdgeInsets.all(10),
-                            child: child)),
-                    ...widget.overlayWidgets
-                  ]),
-                  bottomNavigationBar: bnb != null
-                      ? createControl(control, bnb.id, control.isDisabled)
-                      : null,
-                  floatingActionButton: fab != null
-                      ? createControl(control, fab.id, control.isDisabled)
-                      : null,
-                  floatingActionButtonLocation: fabLocation,
-                );
-
-                return Directionality(
-                    textDirection: textDirection,
-                    child: widget.loadingPage != null
-                        ? Stack(
-                            children: [scaffold, widget.loadingPage!],
-                          )
-                        : scaffold);
-              });
+                        bgcolor: HexColor.fromString(
+                            Theme.of(context),
+                            cupertinoAppBarView.control
+                                .attrString("bgcolor", "")!),
+                      ) as ObstructingPreferredSizeWidget
+                    : null;
+
+            var scaffold = Scaffold(
+              key: scaffoldKey,
+              backgroundColor: HexColor.fromString(
+                  Theme.of(context), control.attrString("bgcolor", "")!),
+              appBar: bar,
+              drawer: drawerView != null
+                  ? NavigationDrawerControl(
+                      control: drawerView.control,
+                      children: drawerView.children,
+                      parentDisabled: control.isDisabled,
+                    )
+                  : null,
+              onDrawerChanged: (opened) {
+                if (drawerView != null && !opened) {
+                  widget.parent.state["drawerOpened"] = false;
+                  dismissDrawer(drawerView.control.id);
+                }
+              },
+              endDrawer: endDrawerView != null
+                  ? NavigationDrawerControl(
+                      control: endDrawerView.control,
+                      children: endDrawerView.children,
+                      parentDisabled: control.isDisabled,
+                    )
+                  : null,
+              onEndDrawerChanged: (opened) {
+                if (endDrawerView != null && !opened) {
+                  widget.parent.state["endDrawerOpened"] = false;
+                  dismissDrawer(endDrawerView.control.id);
+                }
+              },
+              body: Stack(children: [
+                SizedBox.expand(
+                    child: Container(
+                        padding: parseEdgeInsets(control, "padding") ??
+                            const EdgeInsets.all(10),
+                        child: child)),
+                ...widget.overlayWidgets
+              ]),
+              bottomNavigationBar: bnb != null
+                  ? createControl(control, bnb.id, control.isDisabled)
+                  : null,
+              floatingActionButton: fab != null
+                  ? createControl(control, fab.id, control.isDisabled)
+                  : null,
+              floatingActionButtonLocation: fabLocation,
+            );
+
+            return Directionality(
+                textDirection: textDirection,
+                child: widget.loadingPage != null
+                    ? Stack(
+                        children: [scaffold, widget.loadingPage!],
+                      )
+                    : scaffold);
+          });
         });
   }
 }
diff --git a/package/lib/src/controls/pagelet.dart b/package/lib/src/controls/pagelet.dart
index 325eba5bd..8dffe4ba5 100644
--- a/package/lib/src/controls/pagelet.dart
+++ b/package/lib/src/controls/pagelet.dart
@@ -3,17 +3,15 @@ import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
 import '../models/app_state.dart';
 import '../models/control.dart';
 import '../models/controls_view_model.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/colors.dart';
 import 'app_bar.dart';
 import 'create_control.dart';
 import 'cupertino_app_bar.dart';
 import 'error.dart';
+import 'flet_control_stateful_mixin.dart';
 import 'floating_action_button.dart';
 import 'navigation_drawer.dart';
 
@@ -22,21 +20,20 @@ class PageletControl extends StatefulWidget {
   final Control control;
   final List<Control> children;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const PageletControl(
       {super.key,
       this.parent,
       required this.control,
       required this.children,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<PageletControl> createState() => _PageletControlState();
 }
 
-class _PageletControlState extends State<PageletControl> {
+class _PageletControlState extends State<PageletControl>
+    with FletControlStatefulMixin {
   final scaffoldKey = GlobalKey<ScaffoldState>();
 
   @override
@@ -115,14 +112,8 @@ class _PageletControlState extends State<PageletControl> {
               FloatingActionButtonLocation.endFloat);
 
           void dismissDrawer(String id) {
-            List<Map<String, String>> props = [
-              {"i": id, "open": "false"}
-            ];
-            widget.dispatch(UpdateControlPropsAction(
-                UpdateControlPropsPayload(props: props)));
-            FletAppServices.of(context).server.updateControlProps(props: props);
-            FletAppServices.of(context).server.sendPageEvent(
-                eventTarget: id, eventName: "dismiss", eventData: "");
+            updateControlProps(id, {"open": "false"});
+            sendControlEvent(id, "dismiss", "");
           }
 
           WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -202,7 +193,6 @@ class _PageletControlState extends State<PageletControl> {
                           control: drawerView.control,
                           children: drawerView.children,
                           parentDisabled: widget.control.isDisabled,
-                          dispatch: widget.dispatch,
                         )
                       : null,
                   onDrawerChanged: (opened) {
@@ -216,7 +206,6 @@ class _PageletControlState extends State<PageletControl> {
                           control: endDrawerView.control,
                           children: endDrawerView.children,
                           parentDisabled: widget.control.isDisabled,
-                          dispatch: widget.dispatch,
                         )
                       : null,
                   onEndDrawerChanged: (opened) {
diff --git a/package/lib/src/controls/piechart.dart b/package/lib/src/controls/piechart.dart
index 9e8933aec..21b221937 100644
--- a/package/lib/src/controls/piechart.dart
+++ b/package/lib/src/controls/piechart.dart
@@ -1,20 +1,78 @@
 import 'dart:convert';
 
+import 'package:collection/collection.dart';
+import 'package:equatable/equatable.dart';
 import 'package:fl_chart/fl_chart.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_redux/flutter_redux.dart';
+import 'package:redux/redux.dart';
 
-import '../flet_app_services.dart';
 import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/piechart_event_data.dart';
-import '../models/piechart_section_view_model.dart';
-import '../models/piechart_view_model.dart';
 import '../utils/animations.dart';
 import '../utils/borders.dart';
 import '../utils/colors.dart';
 import '../utils/text.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
+
+class PieChartEventData extends Equatable {
+  final String eventType;
+  final int? sectionIndex;
+  // final double? angle;
+  // final double? radius;
+
+  const PieChartEventData(
+      {required this.eventType, required this.sectionIndex});
+
+  Map<String, dynamic> toJson() =>
+      <String, dynamic>{'type': eventType, 'section_index': sectionIndex};
+
+  @override
+  List<Object?> get props => [eventType, sectionIndex];
+}
+
+class PieChartSectionViewModel extends Equatable {
+  final Control control;
+  final Control? badge;
+
+  const PieChartSectionViewModel({required this.control, required this.badge});
+
+  static PieChartSectionViewModel fromStore(
+      Store<AppState> store, Control control) {
+    var children = store.state.controls[control.id]!.childIds
+        .map((childId) => store.state.controls[childId])
+        .whereNotNull()
+        .where((c) => c.isVisible);
+
+    return PieChartSectionViewModel(
+        control: control,
+        badge: children.firstWhereOrNull((c) => c.name == "badge"));
+  }
+
+  @override
+  List<Object?> get props => [control, badge];
+}
+
+class PieChartViewModel extends Equatable {
+  final Control control;
+  final List<PieChartSectionViewModel> sections;
+
+  const PieChartViewModel({required this.control, required this.sections});
+
+  static PieChartViewModel fromStore(
+      Store<AppState> store, Control control, List<Control> children) {
+    return PieChartViewModel(
+        control: control,
+        sections: children
+            .where((c) => c.type == "section" && c.isVisible)
+            .map((c) => PieChartSectionViewModel.fromStore(store, c))
+            .toList());
+  }
+
+  @override
+  List<Object?> get props => [control, sections];
+}
 
 class PieChartControl extends StatefulWidget {
   final Control? parent;
@@ -33,7 +91,8 @@ class PieChartControl extends StatefulWidget {
   State<PieChartControl> createState() => _PieChartControlState();
 }
 
-class _PieChartControlState extends State<PieChartControl> {
+class _PieChartControlState extends State<PieChartControl>
+    with FletControlStatefulMixin {
   PieChartEventData? _eventData;
 
   @override
@@ -79,10 +138,8 @@ class _PieChartControlState extends State<PieChartControl> {
                           _eventData = eventData;
                           debugPrint(
                               "PieChart ${widget.control.id} ${eventData.eventType}");
-                          FletAppServices.of(context).server.sendPageEvent(
-                              eventTarget: widget.control.id,
-                              eventName: "chart_event",
-                              eventData: json.encode(eventData));
+                          sendControlEvent(widget.control.id, "chart_event",
+                              json.encode(eventData));
                         }
                       }
                     : null,
diff --git a/package/lib/src/controls/popup_menu_button.dart b/package/lib/src/controls/popup_menu_button.dart
index 7b36fa47a..93a0d89fc 100644
--- a/package/lib/src/controls/popup_menu_button.dart
+++ b/package/lib/src/controls/popup_menu_button.dart
@@ -1,14 +1,13 @@
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/controls_view_model.dart';
 import '../utils/icons.dart';
 import 'create_control.dart';
+import 'flet_control_stateless_mixin.dart';
+import 'flet_store_mixin.dart';
 
-class PopupMenuButtonControl extends StatelessWidget {
+class PopupMenuButtonControl extends StatelessWidget
+    with FletControlStatelessMixin, FletStoreMixin {
   final Control? parent;
   final Control control;
   final bool parentDisabled;
@@ -25,8 +24,6 @@ class PopupMenuButtonControl extends StatelessWidget {
   Widget build(BuildContext context) {
     debugPrint("PopupMenuButton build: ${control.id}");
 
-    final server = FletAppServices.of(context).server;
-
     var icon = parseIcon(control.attrString("icon", "")!);
     var tooltip = control.attrString("tooltip");
     var contentCtrls =
@@ -37,69 +34,60 @@ class PopupMenuButtonControl extends StatelessWidget {
         ? createControl(control, contentCtrls.first.id, disabled)
         : null;
 
-    var popupButton = StoreConnector<AppState, ControlsViewModel>(
-        distinct: true,
-        converter: (store) => ControlsViewModel.fromStore(
-            store, children.where((c) => c.name != "content").map((c) => c.id)),
-        builder: (content, viewModel) {
-          return PopupMenuButton<String>(
-              enabled: !disabled,
-              icon: icon != null ? Icon(icon) : null,
-              tooltip: tooltip,
-              shape: Theme.of(context).useMaterial3
-                  ? RoundedRectangleBorder(
-                      borderRadius: BorderRadius.circular(10))
-                  : null,
-              onCanceled: () {
-                server.sendPageEvent(
-                    eventTarget: control.id,
-                    eventName: "cancelled",
-                    eventData: "");
-              },
-              onSelected: (itemId) {
-                server.sendPageEvent(
-                    eventTarget: itemId, eventName: "click", eventData: "");
-              },
-              itemBuilder: (BuildContext context) =>
-                  viewModel.controlViews.map((cv) {
-                    var itemIcon =
-                        parseIcon(cv.control.attrString("icon", "")!);
-                    var text = cv.control.attrString("text", "")!;
-                    var checked = cv.control.attrBool("checked");
-                    var contentCtrls =
-                        cv.children.where((c) => c.name == "content");
+    var popupButton = withControls(
+        children.where((c) => c.name != "content").map((c) => c.id),
+        (content, viewModel) {
+      return PopupMenuButton<String>(
+          enabled: !disabled,
+          icon: icon != null ? Icon(icon) : null,
+          tooltip: tooltip,
+          shape: Theme.of(context).useMaterial3
+              ? RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))
+              : null,
+          onCanceled: () {
+            sendControlEvent(context, control.id, "cancelled", "");
+          },
+          onSelected: (itemId) {
+            sendControlEvent(context, itemId, "click", "");
+          },
+          itemBuilder: (BuildContext context) =>
+              viewModel.controlViews.map((cv) {
+                var itemIcon = parseIcon(cv.control.attrString("icon", "")!);
+                var text = cv.control.attrString("text", "")!;
+                var checked = cv.control.attrBool("checked");
+                var contentCtrls =
+                    cv.children.where((c) => c.name == "content");
 
-                    Widget? child;
-                    if (contentCtrls.isNotEmpty) {
-                      // custom content
-                      child = createControl(
-                          cv.control, contentCtrls.first.id, parentDisabled);
-                    } else if (itemIcon != null && text != "") {
-                      // icon and text
-                      child = Row(children: [
-                        Icon(itemIcon),
-                        const SizedBox(width: 8),
-                        Text(text)
-                      ]);
-                    } else if (text != "") {
-                      child = Text(text);
-                    }
+                Widget? child;
+                if (contentCtrls.isNotEmpty) {
+                  // custom content
+                  child = createControl(
+                      cv.control, contentCtrls.first.id, parentDisabled);
+                } else if (itemIcon != null && text != "") {
+                  // icon and text
+                  child = Row(children: [
+                    Icon(itemIcon),
+                    const SizedBox(width: 8),
+                    Text(text)
+                  ]);
+                } else if (text != "") {
+                  child = Text(text);
+                }
 
-                    var item = checked != null
-                        ? CheckedPopupMenuItem<String>(
-                            value: cv.control.id,
-                            checked: checked,
-                            child: child,
-                          )
-                        : PopupMenuItem<String>(
-                            value: cv.control.id, child: child);
+                var item = checked != null
+                    ? CheckedPopupMenuItem<String>(
+                        value: cv.control.id,
+                        checked: checked,
+                        child: child,
+                      )
+                    : PopupMenuItem<String>(value: cv.control.id, child: child);
 
-                    return child != null
-                        ? item
-                        : const PopupMenuDivider() as PopupMenuEntry<String>;
-                  }).toList(),
-              child: child);
-        });
+                return child != null
+                    ? item
+                    : const PopupMenuDivider() as PopupMenuEntry<String>;
+              }).toList(),
+          child: child);
+    });
 
     return constrainedControl(context, popupButton, parent, control);
   }
diff --git a/package/lib/src/controls/radio.dart b/package/lib/src/controls/radio.dart
index 118dbe422..326d63790 100644
--- a/package/lib/src/controls/radio.dart
+++ b/package/lib/src/controls/radio.dart
@@ -1,18 +1,12 @@
-import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/control_ancestor_view_model.dart';
-import '../protocol/update_control_props_payload.dart';
-import '../utils/buttons.dart';
 import '../utils/colors.dart';
 import 'create_control.dart';
 import 'cupertino_radio.dart';
 import 'error.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
 import 'list_tile.dart';
 
 enum LabelPosition { right, left }
@@ -21,20 +15,19 @@ class RadioControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const RadioControl(
       {super.key,
       this.parent,
       required this.control,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<RadioControl> createState() => _RadioControlState();
 }
 
-class _RadioControlState extends State<RadioControl> {
+class _RadioControlState extends State<RadioControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   late final FocusNode _focusNode;
 
   @override
@@ -45,10 +38,8 @@ class _RadioControlState extends State<RadioControl> {
   }
 
   void _onFocusChange() {
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   @override
@@ -61,100 +52,86 @@ class _RadioControlState extends State<RadioControl> {
   void _onChange(String ancestorId, String? value) {
     var svalue = value ?? "";
     debugPrint(svalue);
-    List<Map<String, String>> props = [
-      {"i": ancestorId, "value": svalue}
-    ];
-    widget.dispatch(
-        UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-
-    final server = FletAppServices.of(context).server;
-    server.updateControlProps(props: props);
-    server.sendPageEvent(
-        eventTarget: ancestorId, eventName: "change", eventData: svalue);
+    updateControlProps(ancestorId, {"value": svalue});
+    sendControlEvent(ancestorId, "change", svalue);
   }
 
   @override
   Widget build(BuildContext context) {
     debugPrint("Radio build: ${widget.control.id}");
 
-    bool adaptive = widget.control.attrBool("adaptive", false)!;
-    if (adaptive &&
-        (defaultTargetPlatform == TargetPlatform.iOS ||
-            defaultTargetPlatform == TargetPlatform.macOS)) {
-      return CupertinoRadioControl(
-          control: widget.control,
-          parentDisabled: widget.parentDisabled,
-          dispatch: widget.dispatch);
-    }
-
-    String label = widget.control.attrString("label", "")!;
-    String value = widget.control.attrString("value", "")!;
-    LabelPosition labelPosition = LabelPosition.values.firstWhere(
-        (p) =>
-            p.name.toLowerCase() ==
-            widget.control.attrString("labelPosition", "")!.toLowerCase(),
-        orElse: () => LabelPosition.right);
-    bool autofocus = widget.control.attrBool("autofocus", false)!;
-    bool disabled = widget.control.isDisabled || widget.parentDisabled;
-
-    return StoreConnector<AppState, ControlAncestorViewModel>(
-        distinct: true,
-        ignoreChange: (state) {
-          return state.controls[widget.control.id] == null;
-        },
-        converter: (store) => ControlAncestorViewModel.fromStore(
-            store, widget.control.id, "radiogroup"),
-        builder: (context, viewModel) {
-          debugPrint("Radio StoreConnector build: ${widget.control.id}");
-
-          if (viewModel.ancestor == null) {
-            return const ErrorControl(
-                "Radio control must be enclosed with RadioGroup.");
-          }
-
-          String groupValue = viewModel.ancestor!.attrString("value", "")!;
-          String ancestorId = viewModel.ancestor!.id;
-
-          var radio = Radio<String>(
-              autofocus: autofocus,
-              focusNode: _focusNode,
-              groupValue: groupValue,
-              value: value,
-              activeColor: HexColor.fromString(Theme.of(context),
-                  widget.control.attrString("activeColor", "")!),
-              fillColor: parseMaterialStateColor(
-                  Theme.of(context), widget.control, "fillColor"),
-              onChanged: !disabled
-                  ? (String? value) {
-                      _onChange(ancestorId, value);
-                    }
-                  : null);
-
-          ListTileClicks.of(context)?.notifier.addListener(() {
-            _onChange(ancestorId, value);
-          });
-
-          Widget result = radio;
-          if (label != "") {
-            var labelWidget = disabled
-                ? Text(label,
-                    style: TextStyle(color: Theme.of(context).disabledColor))
-                : MouseRegion(
-                    cursor: SystemMouseCursors.click, child: Text(label));
-            result = MergeSemantics(
-                child: GestureDetector(
-                    onTap: !disabled
-                        ? () {
-                            _onChange(ancestorId, value);
-                          }
-                        : null,
-                    child: labelPosition == LabelPosition.right
-                        ? Row(children: [radio, labelWidget])
-                        : Row(children: [labelWidget, radio])));
-          }
-
-          return constrainedControl(
-              context, result, widget.parent, widget.control);
+    return withPagePlatform((context, platform) {
+      bool adaptive = widget.control.attrBool("adaptive", false)!;
+      if (adaptive &&
+          (platform == TargetPlatform.iOS ||
+              platform == TargetPlatform.macOS)) {
+        return CupertinoRadioControl(
+            control: widget.control, parentDisabled: widget.parentDisabled);
+      }
+
+      String label = widget.control.attrString("label", "")!;
+      String value = widget.control.attrString("value", "")!;
+      LabelPosition labelPosition = LabelPosition.values.firstWhere(
+          (p) =>
+              p.name.toLowerCase() ==
+              widget.control.attrString("labelPosition", "")!.toLowerCase(),
+          orElse: () => LabelPosition.right);
+      bool autofocus = widget.control.attrBool("autofocus", false)!;
+      bool disabled = widget.control.isDisabled || widget.parentDisabled;
+
+      return withControlAncestor(widget.control.id, "radiogroup",
+          (context, viewModel) {
+        debugPrint("Radio StoreConnector build: ${widget.control.id}");
+
+        if (viewModel.ancestor == null) {
+          return const ErrorControl(
+              "Radio control must be enclosed with RadioGroup.");
+        }
+
+        String groupValue = viewModel.ancestor!.attrString("value", "")!;
+        String ancestorId = viewModel.ancestor!.id;
+
+        var radio = Radio<String>(
+            autofocus: autofocus,
+            focusNode: _focusNode,
+            groupValue: groupValue,
+            value: value,
+            activeColor: HexColor.fromString(Theme.of(context),
+                widget.control.attrString("activeColor", "")!),
+            fillColor: parseMaterialStateColor(
+                Theme.of(context), widget.control, "fillColor"),
+            onChanged: !disabled
+                ? (String? value) {
+                    _onChange(ancestorId, value);
+                  }
+                : null);
+
+        ListTileClicks.of(context)?.notifier.addListener(() {
+          _onChange(ancestorId, value);
         });
+
+        Widget result = radio;
+        if (label != "") {
+          var labelWidget = disabled
+              ? Text(label,
+                  style: TextStyle(color: Theme.of(context).disabledColor))
+              : MouseRegion(
+                  cursor: SystemMouseCursors.click, child: Text(label));
+          result = MergeSemantics(
+              child: GestureDetector(
+                  onTap: !disabled
+                      ? () {
+                          _onChange(ancestorId, value);
+                        }
+                      : null,
+                  child: labelPosition == LabelPosition.right
+                      ? Row(children: [radio, labelWidget])
+                      : Row(children: [labelWidget, radio])));
+        }
+
+        return constrainedControl(
+            context, result, widget.parent, widget.control);
+      });
+    });
   }
 }
diff --git a/package/lib/src/controls/range_slider.dart b/package/lib/src/controls/range_slider.dart
index 1818093c3..381f59f57 100644
--- a/package/lib/src/controls/range_slider.dart
+++ b/package/lib/src/controls/range_slider.dart
@@ -1,33 +1,30 @@
 import 'package:flutter/material.dart';
-import '../actions.dart';
-import '../flet_app_services.dart';
+
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/colors.dart';
+import '../utils/debouncer.dart';
 import '../utils/desktop.dart';
 import 'create_control.dart';
-import '../utils/buttons.dart';
-import '../utils/debouncer.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class RangeSliderControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const RangeSliderControl({
     super.key,
     this.parent,
     required this.control,
     required this.parentDisabled,
-    required this.dispatch,
   });
 
   @override
   State<RangeSliderControl> createState() => _SliderControlState();
 }
 
-class _SliderControlState extends State<RangeSliderControl> {
+class _SliderControlState extends State<RangeSliderControl>
+    with FletControlStatefulMixin {
   final _debouncer = Debouncer(milliseconds: isDesktop() ? 10 : 100);
 
   @override
@@ -42,24 +39,14 @@ class _SliderControlState extends State<RangeSliderControl> {
   }
 
   void onChange(double startValue, double endValue) {
-    var strStartValue = startValue.toString();
-    var strEndValue = endValue.toString();
-
-    List<Map<String, String>> props = [
-      {
-        "i": widget.control.id,
-        "startvalue": strStartValue,
-        "endvalue": strEndValue
-      }
-    ];
-    widget.dispatch(
-        UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-
+    var props = {
+      "startvalue": startValue.toString(),
+      "endvalue": endValue.toString()
+    };
+    updateControlProps(widget.control.id, props, clientOnly: true);
     _debouncer.run(() {
-      final server = FletAppServices.of(context).server;
-      server.updateControlProps(props: props);
-      server.sendPageEvent(
-          eventTarget: widget.control.id, eventName: "change", eventData: '');
+      updateControlProps(widget.control.id, props);
+      sendControlEvent(widget.control.id, "change", "");
     });
   }
 
@@ -78,9 +65,7 @@ class _SliderControlState extends State<RangeSliderControl> {
     int? divisions = widget.control.attrInt("divisions");
     int round = widget.control.attrInt("round", 0)!;
 
-    final server = FletAppServices.of(context).server;
-
-    debugPrint("SliderControl StoreConnector build: ${widget.control.id}");
+    debugPrint("SliderControl build: ${widget.control.id}");
 
     var rangeSlider = RangeSlider(
         values: RangeValues(startValue, endValue),
@@ -105,18 +90,12 @@ class _SliderControlState extends State<RangeSliderControl> {
             : null,
         onChangeStart: !disabled
             ? (RangeValues newValues) {
-                server.sendPageEvent(
-                    eventTarget: widget.control.id,
-                    eventName: "change_start",
-                    eventData: '');
+                sendControlEvent(widget.control.id, "change_start", '');
               }
             : null,
         onChangeEnd: !disabled
             ? (RangeValues newValues) {
-                server.sendPageEvent(
-                    eventTarget: widget.control.id,
-                    eventName: "change_end",
-                    eventData: '');
+                sendControlEvent(widget.control.id, "change_end", '');
               }
             : null);
 
diff --git a/package/lib/src/controls/responsive_row.dart b/package/lib/src/controls/responsive_row.dart
index 02d9423d3..1a7d28244 100644
--- a/package/lib/src/controls/responsive_row.dart
+++ b/package/lib/src/controls/responsive_row.dart
@@ -1,15 +1,15 @@
 import 'package:flutter/widgets.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/page_size_view_model.dart';
 import '../utils/alignment.dart';
 import '../utils/responsive.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateless_mixin.dart';
+import 'flet_store_mixin.dart';
 
-class ResponsiveRowControl extends StatelessWidget {
+class ResponsiveRowControl extends StatelessWidget
+    with FletControlStatelessMixin, FletStoreMixin {
   final Control? parent;
   final Control control;
   final bool parentDisabled;
@@ -31,86 +31,82 @@ class ResponsiveRowControl extends StatelessWidget {
     final runSpacing = parseResponsiveNumber(control, "runSpacing", 10);
     bool disabled = control.isDisabled || parentDisabled;
 
-    return StoreConnector<AppState, PageSizeViewModel>(
-        distinct: true,
-        converter: (store) => PageSizeViewModel.fromStore(store),
-        builder: (context, view) {
-          var w = LayoutBuilder(
-              builder: (BuildContext context, BoxConstraints constraints) {
-            debugPrint(
-                "ResponsiveRow constraints.maxWidth: ${constraints.maxWidth}");
-            debugPrint(
-                "ResponsiveRow constraints.maxHeight: ${constraints.maxHeight}");
+    return withPageSize((context, view) {
+      var w = LayoutBuilder(
+          builder: (BuildContext context, BoxConstraints constraints) {
+        debugPrint(
+            "ResponsiveRow constraints.maxWidth: ${constraints.maxWidth}");
+        debugPrint(
+            "ResponsiveRow constraints.maxHeight: ${constraints.maxHeight}");
 
-            var bpSpacing =
-                getBreakpointNumber(spacing, view.size.width, view.breakpoints);
+        var bpSpacing =
+            getBreakpointNumber(spacing, view.size.width, view.breakpoints);
 
-            var bpColumns =
-                getBreakpointNumber(columns, view.size.width, view.breakpoints);
+        var bpColumns =
+            getBreakpointNumber(columns, view.size.width, view.breakpoints);
 
-            double totalCols = 0;
-            List<Widget> controls = [];
-            for (var ctrl in children.where((c) => c.isVisible)) {
-              final col = parseResponsiveNumber(ctrl, "col", 12);
-              var bpCol =
-                  getBreakpointNumber(col, view.size.width, view.breakpoints);
-              totalCols += bpCol;
+        double totalCols = 0;
+        List<Widget> controls = [];
+        for (var ctrl in children.where((c) => c.isVisible)) {
+          final col = parseResponsiveNumber(ctrl, "col", 12);
+          var bpCol =
+              getBreakpointNumber(col, view.size.width, view.breakpoints);
+          totalCols += bpCol;
 
-              // calculate child width
-              var colWidth =
-                  (constraints.maxWidth - bpSpacing * (bpColumns - 1)) /
-                      bpColumns;
-              var childWidth = colWidth * bpCol + bpSpacing * (bpCol - 1);
+          // calculate child width
+          var colWidth =
+              (constraints.maxWidth - bpSpacing * (bpColumns - 1)) / bpColumns;
+          var childWidth = colWidth * bpCol + bpSpacing * (bpCol - 1);
 
-              controls.add(ConstrainedBox(
-                constraints: BoxConstraints(
-                  minWidth: childWidth,
-                  maxWidth: childWidth,
-                ),
-                child: createControl(control, ctrl.id, disabled),
-              ));
-            }
+          controls.add(ConstrainedBox(
+            constraints: BoxConstraints(
+              minWidth: childWidth,
+              maxWidth: childWidth,
+            ),
+            child: createControl(control, ctrl.id, disabled),
+          ));
+        }
 
-            var wrap = (totalCols > bpColumns);
+        var wrap = (totalCols > bpColumns);
 
-            if (!wrap && bpSpacing > 0) {
-              var i = 1;
-              while (i < controls.length) {
-                controls.insert(i, SizedBox(width: bpSpacing));
-                i += 2;
-              }
-            }
+        if (!wrap && bpSpacing > 0) {
+          var i = 1;
+          while (i < controls.length) {
+            controls.insert(i, SizedBox(width: bpSpacing));
+            i += 2;
+          }
+        }
 
-            try {
-              return wrap
-                  ? Wrap(
-                      direction: Axis.horizontal,
-                      spacing: bpSpacing - 0.1,
-                      runSpacing: getBreakpointNumber(
-                          runSpacing, view.size.width, view.breakpoints),
-                      alignment: parseWrapAlignment(
-                          control, "alignment", WrapAlignment.start),
-                      crossAxisAlignment: parseWrapCrossAlignment(control,
-                          "verticalAlignment", WrapCrossAlignment.start),
-                      children: controls,
-                    )
-                  : Row(
-                      mainAxisAlignment: parseMainAxisAlignment(
-                          control, "alignment", MainAxisAlignment.start),
-                      mainAxisSize: MainAxisSize.max,
-                      crossAxisAlignment: parseCrossAxisAlignment(control,
-                          "verticalAlignment", CrossAxisAlignment.start),
-                      children: controls,
-                    );
-            } catch (e) {
-              return ErrorControl(
-                "Error displaying ResponsiveRow",
-                description: e.toString(),
-              );
-            }
-          });
+        try {
+          return wrap
+              ? Wrap(
+                  direction: Axis.horizontal,
+                  spacing: bpSpacing - 0.1,
+                  runSpacing: getBreakpointNumber(
+                      runSpacing, view.size.width, view.breakpoints),
+                  alignment: parseWrapAlignment(
+                      control, "alignment", WrapAlignment.start),
+                  crossAxisAlignment: parseWrapCrossAlignment(
+                      control, "verticalAlignment", WrapCrossAlignment.start),
+                  children: controls,
+                )
+              : Row(
+                  mainAxisAlignment: parseMainAxisAlignment(
+                      control, "alignment", MainAxisAlignment.start),
+                  mainAxisSize: MainAxisSize.max,
+                  crossAxisAlignment: parseCrossAxisAlignment(
+                      control, "verticalAlignment", CrossAxisAlignment.start),
+                  children: controls,
+                );
+        } catch (e) {
+          return ErrorControl(
+            "Error displaying ResponsiveRow",
+            description: e.toString(),
+          );
+        }
+      });
 
-          return constrainedControl(context, w, parent, control);
-        });
+      return constrainedControl(context, w, parent, control);
+    });
   }
 }
diff --git a/package/lib/src/controls/row.dart b/package/lib/src/controls/row.dart
index 5810ac3ea..2f631fca3 100644
--- a/package/lib/src/controls/row.dart
+++ b/package/lib/src/controls/row.dart
@@ -11,15 +11,13 @@ class RowControl extends StatelessWidget {
   final Control control;
   final bool parentDisabled;
   final List<Control> children;
-  final dynamic dispatch;
 
   const RowControl(
       {super.key,
       this.parent,
       required this.control,
       required this.children,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   Widget build(BuildContext context) {
@@ -73,7 +71,6 @@ class RowControl extends StatelessWidget {
     child = ScrollableControl(
       control: control,
       scrollDirection: wrap ? Axis.vertical : Axis.horizontal,
-      dispatch: dispatch,
       child: child,
     );
 
diff --git a/package/lib/src/controls/scroll_notification_control.dart b/package/lib/src/controls/scroll_notification_control.dart
index 010320b69..9dfd737de 100644
--- a/package/lib/src/controls/scroll_notification_control.dart
+++ b/package/lib/src/controls/scroll_notification_control.dart
@@ -2,8 +2,8 @@ import 'dart:convert';
 
 import 'package:flutter/widgets.dart';
 
-import '../flet_app_services.dart';
 import '../models/control.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class ScrollNotificationControl extends StatefulWidget {
   final Widget child;
@@ -17,7 +17,8 @@ class ScrollNotificationControl extends StatefulWidget {
       _ScrollNotificationControlState();
 }
 
-class _ScrollNotificationControlState extends State<ScrollNotificationControl> {
+class _ScrollNotificationControlState extends State<ScrollNotificationControl>
+    with FletControlStatefulMixin {
   int _onScrollInterval = 0;
   final Map<String, int> _lastEventTimestamps = {};
 
@@ -41,8 +42,7 @@ class _ScrollNotificationControlState extends State<ScrollNotificationControl> {
       }
 
       debugPrint("ScrollNotification ${widget.control.id} event");
-      FletAppServices.of(context).server.sendPageEvent(
-          eventTarget: widget.control.id, eventName: "onScroll", eventData: d);
+      sendControlEvent(widget.control.id, "onScroll", d);
     }
 
     if (notification.depth == 0) {
diff --git a/package/lib/src/controls/scrollable_control.dart b/package/lib/src/controls/scrollable_control.dart
index f9e01ca31..f00342069 100644
--- a/package/lib/src/controls/scrollable_control.dart
+++ b/package/lib/src/controls/scrollable_control.dart
@@ -3,14 +3,13 @@ import 'dart:convert';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 
-import '../actions.dart';
 import '../flet_app_services.dart';
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/animations.dart';
 import '../utils/desktop.dart';
 import '../utils/numbers.dart';
 import '../widgets/adjustable_scroll_controller.dart';
+import 'flet_control_stateful_mixin.dart';
 
 enum ScrollMode { none, auto, adaptive, always, hidden }
 
@@ -19,21 +18,21 @@ class ScrollableControl extends StatefulWidget {
   final Widget child;
   final Axis scrollDirection;
   final ScrollController? scrollController;
-  final dynamic dispatch;
 
-  const ScrollableControl(
-      {super.key,
-      required this.control,
-      required this.child,
-      required this.scrollDirection,
-      this.scrollController,
-      required this.dispatch});
+  const ScrollableControl({
+    super.key,
+    required this.control,
+    required this.child,
+    required this.scrollDirection,
+    this.scrollController,
+  });
 
   @override
   State<ScrollableControl> createState() => _ScrollableControlState();
 }
 
-class _ScrollableControlState extends State<ScrollableControl> {
+class _ScrollableControlState extends State<ScrollableControl>
+    with FletControlStatefulMixin {
   late final ScrollController _controller;
   late bool _ownController = false;
   String? _method;
@@ -88,13 +87,7 @@ class _ScrollableControlState extends State<ScrollableControl> {
     } else if (method != null && method != _method) {
       _method = method;
       debugPrint("ScrollableControl JSON method: $method");
-
-      List<Map<String, String>> props = [
-        {"i": widget.control.id, "method": ""}
-      ];
-      widget.dispatch(
-          UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-      FletAppServices.of(context).server.updateControlProps(props: props);
+      updateControlProps(widget.control.id, {"method": ""});
 
       var mj = json.decode(method);
       var name = mj["n"] as String;
diff --git a/package/lib/src/controls/search_anchor.dart b/package/lib/src/controls/search_anchor.dart
index a54e873e3..bcbe6c373 100644
--- a/package/lib/src/controls/search_anchor.dart
+++ b/package/lib/src/controls/search_anchor.dart
@@ -1,39 +1,33 @@
 import 'dart:convert';
 
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/borders.dart';
-import '../utils/buttons.dart';
 import '../utils/colors.dart';
 import '../utils/text.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class SearchAnchorControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final List<Control> children;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const SearchAnchorControl(
       {super.key,
       this.parent,
       required this.control,
       required this.children,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<SearchAnchorControl> createState() => _SearchAnchorControlState();
 }
 
-class _SearchAnchorControlState extends State<SearchAnchorControl> {
+class _SearchAnchorControlState extends State<SearchAnchorControl>
+    with FletControlStatefulMixin {
   late final SearchController _controller;
 
   @override
@@ -57,12 +51,7 @@ class _SearchAnchorControlState extends State<SearchAnchorControl> {
 
   void _updateValue(String value) {
     debugPrint("SearchBar.changeValue: $value");
-    List<Map<String, String>> props = [
-      {"i": widget.control.id, "value": value}
-    ];
-    widget.dispatch(
-        UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-    FletAppServices.of(context).server.updateControlProps(props: props);
+    updateControlProps(widget.control.id, {"value": value});
   }
 
   @override
@@ -72,162 +61,138 @@ class _SearchAnchorControlState extends State<SearchAnchorControl> {
 
     debugPrint(widget.control.attrs.toString());
 
-    return StoreConnector<AppState, Function>(
-        distinct: true,
-        converter: (store) => store.dispatch,
-        builder: (context, dispatch) {
-          debugPrint("SearchAnchor StoreConnector build: ${widget.control.id}");
-
-          var value = widget.control.attrString("value");
-          if (value != null && value != _controller.text) {
-            WidgetsBinding.instance.addPostFrameCallback((_) {
-              _controller.text = value;
-            });
-          }
+    debugPrint("SearchAnchor build: ${widget.control.id}");
 
-          bool onChange = widget.control.attrBool("onChange", false)!;
-          bool onTap = widget.control.attrBool("onTap", false)!;
-          bool onSubmit = widget.control.attrBool("onSubmit", false)!;
-
-          var suggestionCtrls =
-              widget.children.where((c) => c.name == "controls" && c.isVisible);
-          var barLeadingCtrls = widget.children
-              .where((c) => c.name == "barLeading" && c.isVisible);
-          var barTrailingCtrls = widget.children
-              .where((c) => c.name == "barTrailing" && c.isVisible);
-          var viewLeadingCtrls = widget.children
-              .where((c) => c.name == "viewLeading" && c.isVisible);
-          var viewTrailingCtrls = widget.children
-              .where((c) => c.name == "viewTrailing" && c.isVisible);
-
-          var viewBgcolor = HexColor.fromString(
-              Theme.of(context), widget.control.attrString("viewBgcolor", "")!);
-          var dividerColor = HexColor.fromString(Theme.of(context),
-              widget.control.attrString("dividerColor", "")!);
-
-          TextStyle? viewHeaderTextStyle = parseTextStyle(
-              Theme.of(context), widget.control, "viewHeaderTextStyle");
-          TextStyle? viewHintTextStyle = parseTextStyle(
-              Theme.of(context), widget.control, "viewHintTextStyle");
-
-          var method = widget.control.attrString("method");
-
-          if (method != null) {
-            debugPrint("SearchAnchor JSON method: $method");
-
-            void resetMethod() {
-              List<Map<String, String>> props = [
-                {"i": widget.control.id, "method": ""}
-              ];
-              widget.dispatch(UpdateControlPropsAction(
-                  UpdateControlPropsPayload(props: props)));
-              FletAppServices.of(context)
-                  .server
-                  .updateControlProps(props: props);
-            }
-
-            var mj = json.decode(method);
-            var name = mj["n"] as String;
-            var params = Map<String, dynamic>.from(mj["p"] as Map);
-
-            if (name == "closeView") {
-              WidgetsBinding.instance.addPostFrameCallback((_) {
-                resetMethod();
-                if (_controller.isOpen) {
-                  var text = params["text"].toString();
-                  _updateValue(text);
-                  _controller.closeView(text);
-                }
-              });
-            } else if (name == "openView") {
-              WidgetsBinding.instance.addPostFrameCallback((_) {
-                resetMethod();
-                if (!_controller.isOpen) {
-                  _controller.openView();
-                }
-              });
-            }
+    var value = widget.control.attrString("value");
+    if (value != null && value != _controller.text) {
+      WidgetsBinding.instance.addPostFrameCallback((_) {
+        _controller.text = value;
+      });
+    }
+
+    bool onChange = widget.control.attrBool("onChange", false)!;
+    bool onTap = widget.control.attrBool("onTap", false)!;
+    bool onSubmit = widget.control.attrBool("onSubmit", false)!;
+
+    var suggestionCtrls =
+        widget.children.where((c) => c.name == "controls" && c.isVisible);
+    var barLeadingCtrls =
+        widget.children.where((c) => c.name == "barLeading" && c.isVisible);
+    var barTrailingCtrls =
+        widget.children.where((c) => c.name == "barTrailing" && c.isVisible);
+    var viewLeadingCtrls =
+        widget.children.where((c) => c.name == "viewLeading" && c.isVisible);
+    var viewTrailingCtrls =
+        widget.children.where((c) => c.name == "viewTrailing" && c.isVisible);
+
+    var viewBgcolor = HexColor.fromString(
+        Theme.of(context), widget.control.attrString("viewBgcolor", "")!);
+    var dividerColor = HexColor.fromString(
+        Theme.of(context), widget.control.attrString("dividerColor", "")!);
+
+    TextStyle? viewHeaderTextStyle = parseTextStyle(
+        Theme.of(context), widget.control, "viewHeaderTextStyle");
+    TextStyle? viewHintTextStyle =
+        parseTextStyle(Theme.of(context), widget.control, "viewHintTextStyle");
+
+    var method = widget.control.attrString("method");
+
+    if (method != null) {
+      debugPrint("SearchAnchor JSON method: $method");
+
+      void resetMethod() {
+        updateControlProps(widget.control.id, {"method": ""});
+      }
+
+      var mj = json.decode(method);
+      var name = mj["n"] as String;
+      var params = Map<String, dynamic>.from(mj["p"] as Map);
+
+      if (name == "closeView") {
+        WidgetsBinding.instance.addPostFrameCallback((_) {
+          resetMethod();
+          if (_controller.isOpen) {
+            var text = params["text"].toString();
+            _updateValue(text);
+            _controller.closeView(text);
           }
-
-          Widget anchor = SearchAnchor(
-              searchController: _controller,
-              headerHintStyle: viewHintTextStyle,
-              headerTextStyle: viewHeaderTextStyle,
-              viewSide: parseBorderSide(
-                  Theme.of(context), widget.control, "viewSide"),
-              isFullScreen: widget.control.attrBool("fullScreen", false),
-              viewBackgroundColor: viewBgcolor,
-              dividerColor: dividerColor,
-              viewHintText: widget.control.attrString("viewHintText"),
-              viewElevation: widget.control.attrDouble("viewElevation"),
-              viewShape: parseOutlinedBorder(widget.control, "viewShape"),
-              viewTrailing: viewTrailingCtrls.isNotEmpty
-                  ? viewTrailingCtrls.map((ctrl) {
-                      return createControl(widget.parent, ctrl.id, disabled);
-                    })
-                  : null,
-              viewLeading: viewLeadingCtrls.isNotEmpty
-                  ? createControl(
-                      widget.parent, viewLeadingCtrls.first.id, disabled)
-                  : null,
-              builder: (BuildContext context, SearchController controller) {
-                return SearchBar(
-                  controller: controller,
-                  hintText: widget.control.attrString("barHintText"),
-                  backgroundColor: parseMaterialStateColor(
-                      Theme.of(context), widget.control, "barBgcolor"),
-                  overlayColor: parseMaterialStateColor(
-                      Theme.of(context), widget.control, "barOverlayColor"),
-                  leading: barLeadingCtrls.isNotEmpty
-                      ? createControl(
-                          widget.parent, barLeadingCtrls.first.id, disabled)
-                      : null,
-                  trailing: barTrailingCtrls.isNotEmpty
-                      ? barTrailingCtrls.map((ctrl) {
-                          return createControl(
-                              widget.parent, ctrl.id, disabled);
-                        })
-                      : null,
-                  onTap: () {
-                    if (onTap) {
-                      FletAppServices.of(context).server.sendPageEvent(
-                          eventTarget: widget.control.id,
-                          eventName: "tap",
-                          eventData: "");
-                    }
-                    controller.openView();
-                  },
-                  onSubmitted: onSubmit
-                      ? (String value) {
-                          debugPrint("SearchBar.onSubmit: $value");
-                          _updateValue(value);
-                          FletAppServices.of(context).server.sendPageEvent(
-                              eventTarget: widget.control.id,
-                              eventName: "submit",
-                              eventData: value);
-                        }
-                      : null,
-                  onChanged: onChange
-                      ? (String value) {
-                          debugPrint("SearchBar.onChange: $value");
-                          _updateValue(value);
-                          FletAppServices.of(context).server.sendPageEvent(
-                              eventTarget: widget.control.id,
-                              eventName: "change",
-                              eventData: value);
-                        }
-                      : null,
-                );
-              },
-              suggestionsBuilder:
-                  (BuildContext context, SearchController controller) {
-                return suggestionCtrls.map((ctrl) {
-                  return createControl(widget.parent, ctrl.id, disabled);
-                });
-              });
-
-          return constrainedControl(
-              context, anchor, widget.parent, widget.control);
         });
+      } else if (name == "openView") {
+        WidgetsBinding.instance.addPostFrameCallback((_) {
+          resetMethod();
+          if (!_controller.isOpen) {
+            _controller.openView();
+          }
+        });
+      }
+    }
+
+    Widget anchor = SearchAnchor(
+        searchController: _controller,
+        headerHintStyle: viewHintTextStyle,
+        headerTextStyle: viewHeaderTextStyle,
+        viewSide:
+            parseBorderSide(Theme.of(context), widget.control, "viewSide"),
+        isFullScreen: widget.control.attrBool("fullScreen", false),
+        viewBackgroundColor: viewBgcolor,
+        dividerColor: dividerColor,
+        viewHintText: widget.control.attrString("viewHintText"),
+        viewElevation: widget.control.attrDouble("viewElevation"),
+        viewShape: parseOutlinedBorder(widget.control, "viewShape"),
+        viewTrailing: viewTrailingCtrls.isNotEmpty
+            ? viewTrailingCtrls.map((ctrl) {
+                return createControl(widget.parent, ctrl.id, disabled);
+              })
+            : null,
+        viewLeading: viewLeadingCtrls.isNotEmpty
+            ? createControl(widget.parent, viewLeadingCtrls.first.id, disabled)
+            : null,
+        builder: (BuildContext context, SearchController controller) {
+          return SearchBar(
+            controller: controller,
+            hintText: widget.control.attrString("barHintText"),
+            backgroundColor: parseMaterialStateColor(
+                Theme.of(context), widget.control, "barBgcolor"),
+            overlayColor: parseMaterialStateColor(
+                Theme.of(context), widget.control, "barOverlayColor"),
+            leading: barLeadingCtrls.isNotEmpty
+                ? createControl(
+                    widget.parent, barLeadingCtrls.first.id, disabled)
+                : null,
+            trailing: barTrailingCtrls.isNotEmpty
+                ? barTrailingCtrls.map((ctrl) {
+                    return createControl(widget.parent, ctrl.id, disabled);
+                  })
+                : null,
+            onTap: () {
+              if (onTap) {
+                sendControlEvent(widget.control.id, "tap", "");
+              }
+              controller.openView();
+            },
+            onSubmitted: onSubmit
+                ? (String value) {
+                    debugPrint("SearchBar.onSubmit: $value");
+                    _updateValue(value);
+                    sendControlEvent(widget.control.id, "submit", value);
+                  }
+                : null,
+            onChanged: onChange
+                ? (String value) {
+                    debugPrint("SearchBar.onChange: $value");
+                    _updateValue(value);
+                    sendControlEvent(widget.control.id, "change", value);
+                  }
+                : null,
+          );
+        },
+        suggestionsBuilder:
+            (BuildContext context, SearchController controller) {
+          return suggestionCtrls.map((ctrl) {
+            return createControl(widget.parent, ctrl.id, disabled);
+          });
+        });
+
+    return constrainedControl(context, anchor, widget.parent, widget.control);
   }
 }
diff --git a/package/lib/src/controls/segmented_button.dart b/package/lib/src/controls/segmented_button.dart
index 0e80e14e8..5bfc14650 100644
--- a/package/lib/src/controls/segmented_button.dart
+++ b/package/lib/src/controls/segmented_button.dart
@@ -1,24 +1,19 @@
 import 'dart:convert';
 
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/controls_view_model.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/buttons.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
 
 class SegmentedButtonControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final List<Control> children;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const SegmentedButtonControl({
     super.key,
@@ -26,30 +21,18 @@ class SegmentedButtonControl extends StatefulWidget {
     required this.control,
     required this.children,
     required this.parentDisabled,
-    required this.dispatch,
   });
 
   @override
   State<SegmentedButtonControl> createState() => _SegmentedButtonControlState();
 }
 
-class _SegmentedButtonControlState extends State<SegmentedButtonControl> {
+class _SegmentedButtonControlState extends State<SegmentedButtonControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   void onChange(Set<String> selection) {
     var s = jsonEncode(selection.toList());
-
-    List<Map<String, String>> props = [
-      {
-        "i": widget.control.id,
-        "selected": s,
-      }
-    ];
-    widget.dispatch(
-        UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-
-    final server = FletAppServices.of(context).server;
-    server.updateControlProps(props: props);
-    server.sendPageEvent(
-        eventTarget: widget.control.id, eventName: "change", eventData: s);
+    updateControlProps(widget.control.id, {"selected": s});
+    sendControlEvent(widget.control.id, "change", s);
   }
 
   @override
@@ -106,52 +89,46 @@ class _SegmentedButtonControlState extends State<SegmentedButtonControl> {
     bool showSelectedIcon = widget.control.attrBool("showSelectedIcon", true)!;
 
     bool disabled = widget.control.isDisabled || widget.parentDisabled;
-    debugPrint(
-        "SegmentedButtonControl StoreConnector build: ${widget.control.id}");
-
-    var sb = StoreConnector<AppState, ControlsViewModel>(
-        distinct: true,
-        converter: (store) =>
-            ControlsViewModel.fromStore(store, segments.map((s) => s.id)),
-        builder: (content, segmentViews) {
-          return SegmentedButton<String>(
-              emptySelectionAllowed: allowEmptySelection,
-              multiSelectionEnabled: allowMultipleSelection,
-              selected: selected.isNotEmpty ? selected : {},
-              showSelectedIcon: showSelectedIcon,
-              style: style,
-              selectedIcon: selectedIcon.isNotEmpty
-                  ? createControl(
-                      widget.control, selectedIcon.first.id, disabled)
-                  : null,
-              onSelectionChanged: !disabled
-                  ? (newSelection) {
-                      onChange(newSelection.toSet());
-                    }
-                  : null,
-              segments: segmentViews.controlViews.map((segmentView) {
-                var iconCtrls = segmentView.children
-                    .where((c) => c.name == "icon" && c.isVisible);
-                var labelCtrls = segmentView.children
-                    .where((c) => c.name == "label" && c.isVisible);
-                var enabled = !segmentView.control.attrBool("disabled", false)!;
-
-                return ButtonSegment(
-                    value: segmentView.control.attrString("value")!,
-                    enabled: enabled,
-                    tooltip: enabled && !disabled
-                        ? segmentView.control.attrString("tooltip")
-                        : null,
-                    icon: iconCtrls.isNotEmpty
-                        ? createControl(
-                            segmentView.control, iconCtrls.first.id, disabled)
-                        : null,
-                    label: labelCtrls.isNotEmpty
-                        ? createControl(
-                            segmentView.control, labelCtrls.first.id, disabled)
-                        : null);
-              }).toList());
-        });
+    debugPrint("SegmentedButtonControl build: ${widget.control.id}");
+
+    var sb = withControls(segments.map((s) => s.id), (content, segmentViews) {
+      return SegmentedButton<String>(
+          emptySelectionAllowed: allowEmptySelection,
+          multiSelectionEnabled: allowMultipleSelection,
+          selected: selected.isNotEmpty ? selected : {},
+          showSelectedIcon: showSelectedIcon,
+          style: style,
+          selectedIcon: selectedIcon.isNotEmpty
+              ? createControl(widget.control, selectedIcon.first.id, disabled)
+              : null,
+          onSelectionChanged: !disabled
+              ? (newSelection) {
+                  onChange(newSelection.toSet());
+                }
+              : null,
+          segments: segmentViews.controlViews.map((segmentView) {
+            var iconCtrls = segmentView.children
+                .where((c) => c.name == "icon" && c.isVisible);
+            var labelCtrls = segmentView.children
+                .where((c) => c.name == "label" && c.isVisible);
+            var enabled = !segmentView.control.attrBool("disabled", false)!;
+
+            return ButtonSegment(
+                value: segmentView.control.attrString("value")!,
+                enabled: enabled,
+                tooltip: enabled && !disabled
+                    ? segmentView.control.attrString("tooltip")
+                    : null,
+                icon: iconCtrls.isNotEmpty
+                    ? createControl(
+                        segmentView.control, iconCtrls.first.id, disabled)
+                    : null,
+                label: labelCtrls.isNotEmpty
+                    ? createControl(
+                        segmentView.control, labelCtrls.first.id, disabled)
+                    : null);
+          }).toList());
+    });
 
     return constrainedControl(context, sb, widget.parent, widget.control);
   }
diff --git a/package/lib/src/controls/shake_detector.dart b/package/lib/src/controls/shake_detector.dart
index 6cb6eea10..197a46ac2 100644
--- a/package/lib/src/controls/shake_detector.dart
+++ b/package/lib/src/controls/shake_detector.dart
@@ -4,8 +4,8 @@ import 'dart:math';
 import 'package:flutter/widgets.dart';
 import 'package:sensors_plus/sensors_plus.dart';
 
-import '../flet_app_services.dart';
 import '../models/control.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class ShakeDetectorControl extends StatefulWidget {
   final Control? parent;
@@ -22,7 +22,8 @@ class ShakeDetectorControl extends StatefulWidget {
   State<ShakeDetectorControl> createState() => _ShakeDetectorControlState();
 }
 
-class _ShakeDetectorControlState extends State<ShakeDetectorControl> {
+class _ShakeDetectorControlState extends State<ShakeDetectorControl>
+    with FletControlStatefulMixin {
   ShakeDetector? _shakeDetector;
   int? _minimumShakeCount;
   int? _shakeSlopTimeMs;
@@ -58,10 +59,7 @@ class _ShakeDetectorControlState extends State<ShakeDetectorControl> {
       _shakeDetector?.stopListening();
       _shakeDetector = ShakeDetector.autoStart(
         onPhoneShake: () {
-          FletAppServices.of(context).server.sendPageEvent(
-              eventTarget: widget.control.id,
-              eventName: "shake",
-              eventData: "");
+          sendControlEvent(widget.control.id, "shake", "");
         },
         minimumShakeCount: minimumShakeCount,
         shakeSlopTimeMS: shakeSlopTimeMs,
diff --git a/package/lib/src/controls/slider.dart b/package/lib/src/controls/slider.dart
index c43558d18..a0af2e236 100644
--- a/package/lib/src/controls/slider.dart
+++ b/package/lib/src/controls/slider.dart
@@ -1,33 +1,31 @@
-import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
-import '../actions.dart';
-import '../flet_app_services.dart';
+
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/colors.dart';
-import '../utils/desktop.dart';
 import '../utils/debouncer.dart';
+import '../utils/desktop.dart';
 import 'create_control.dart';
 import 'cupertino_slider.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
 
 class SliderControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const SliderControl(
       {super.key,
       this.parent,
       required this.control,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<SliderControl> createState() => _SliderControlState();
 }
 
-class _SliderControlState extends State<SliderControl> {
+class _SliderControlState extends State<SliderControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   double _value = 0;
   final _debouncer = Debouncer(milliseconds: isDesktop() ? 10 : 100);
   late final FocusNode _focusNode;
@@ -48,30 +46,19 @@ class _SliderControlState extends State<SliderControl> {
   }
 
   void _onFocusChange() {
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   void onChange(double value) {
     var svalue = value.toString();
     debugPrint(svalue);
-    setState(() {
-      _value = value;
-    });
-
-    List<Map<String, String>> props = [
-      {"i": widget.control.id, "value": svalue}
-    ];
-    widget.dispatch(
-        UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-
+    _value = value;
+    var props = {"value": svalue};
+    updateControlProps(widget.control.id, props, clientOnly: true);
     _debouncer.run(() {
-      final server = FletAppServices.of(context).server;
-      server.updateControlProps(props: props);
-      server.sendPageEvent(
-          eventTarget: widget.control.id, eventName: "change", eventData: '');
+      updateControlProps(widget.control.id, props);
+      sendControlEvent(widget.control.id, "change", '');
     });
   }
 
@@ -79,77 +66,71 @@ class _SliderControlState extends State<SliderControl> {
   Widget build(BuildContext context) {
     debugPrint("SliderControl build: ${widget.control.id}");
 
-    bool adaptive = widget.control.attrBool("adaptive", false)!;
-    if (adaptive &&
-        (defaultTargetPlatform == TargetPlatform.iOS ||
-            defaultTargetPlatform == TargetPlatform.macOS)) {
-      return CupertinoSliderControl(
-          control: widget.control,
-          parentDisabled: widget.parentDisabled,
-          dispatch: widget.dispatch);
-    }
-
-    String? label = widget.control.attrString("label");
-    bool autofocus = widget.control.attrBool("autofocus", false)!;
-    bool disabled = widget.control.isDisabled || widget.parentDisabled;
-
-    double min = widget.control.attrDouble("min", 0)!;
-    double max = widget.control.attrDouble("max", 1)!;
-    int? divisions = widget.control.attrInt("divisions");
-    int round = widget.control.attrInt("round", 0)!;
-
-    final server = FletAppServices.of(context).server;
-
-    debugPrint("SliderControl StoreConnector build: ${widget.control.id}");
-
-    double value = widget.control.attrDouble("value", 0)!;
-    if (_value != value) {
-      // verify limits
-      if (value < min) {
-        _value = min;
-      } else if (value > max) {
-        _value = max;
-      } else {
-        _value = value;
+    return withPagePlatform((context, platform) {
+      bool adaptive = widget.control.attrBool("adaptive", false)!;
+      if (adaptive &&
+          (platform == TargetPlatform.iOS ||
+              platform == TargetPlatform.macOS)) {
+        return CupertinoSliderControl(
+            control: widget.control, parentDisabled: widget.parentDisabled);
+      }
+
+      String? label = widget.control.attrString("label");
+      bool autofocus = widget.control.attrBool("autofocus", false)!;
+      bool disabled = widget.control.isDisabled || widget.parentDisabled;
+
+      double min = widget.control.attrDouble("min", 0)!;
+      double max = widget.control.attrDouble("max", 1)!;
+      int? divisions = widget.control.attrInt("divisions");
+      int round = widget.control.attrInt("round", 0)!;
+
+      debugPrint("SliderControl build: ${widget.control.id}");
+
+      double value = widget.control.attrDouble("value", 0)!;
+      if (_value != value) {
+        // verify limits
+        if (value < min) {
+          _value = min;
+        } else if (value > max) {
+          _value = max;
+        } else {
+          _value = value;
+        }
       }
-    }
-
-    var slider = Slider(
-        autofocus: autofocus,
-        focusNode: _focusNode,
-        value: _value,
-        min: min,
-        max: max,
-        divisions: divisions,
-        label: label?.replaceAll("{value}", _value.toStringAsFixed(round)),
-        activeColor: HexColor.fromString(
-            Theme.of(context), widget.control.attrString("activeColor", "")!),
-        inactiveColor: HexColor.fromString(
-            Theme.of(context), widget.control.attrString("inactiveColor", "")!),
-        thumbColor: HexColor.fromString(
-            Theme.of(context), widget.control.attrString("thumbColor", "")!),
-        onChanged: !disabled
-            ? (double value) {
-                onChange(value);
-              }
-            : null,
-        onChangeStart: !disabled
-            ? (double value) {
-                server.sendPageEvent(
-                    eventTarget: widget.control.id,
-                    eventName: "change_start",
-                    eventData: value.toString());
-              }
-            : null,
-        onChangeEnd: !disabled
-            ? (double value) {
-                server.sendPageEvent(
-                    eventTarget: widget.control.id,
-                    eventName: "change_end",
-                    eventData: value.toString());
-              }
-            : null);
-
-    return constrainedControl(context, slider, widget.parent, widget.control);
+
+      var slider = Slider(
+          autofocus: autofocus,
+          focusNode: _focusNode,
+          value: _value,
+          min: min,
+          max: max,
+          divisions: divisions,
+          label: label?.replaceAll("{value}", _value.toStringAsFixed(round)),
+          activeColor: HexColor.fromString(
+              Theme.of(context), widget.control.attrString("activeColor", "")!),
+          inactiveColor: HexColor.fromString(Theme.of(context),
+              widget.control.attrString("inactiveColor", "")!),
+          thumbColor: HexColor.fromString(
+              Theme.of(context), widget.control.attrString("thumbColor", "")!),
+          onChanged: !disabled
+              ? (double value) {
+                  onChange(value);
+                }
+              : null,
+          onChangeStart: !disabled
+              ? (double value) {
+                  sendControlEvent(
+                      widget.control.id, "change_start", value.toString());
+                }
+              : null,
+          onChangeEnd: !disabled
+              ? (double value) {
+                  sendControlEvent(
+                      widget.control.id, "change_end", value.toString());
+                }
+              : null);
+
+      return constrainedControl(context, slider, widget.parent, widget.control);
+    });
   }
 }
diff --git a/package/lib/src/controls/snack_bar.dart b/package/lib/src/controls/snack_bar.dart
index 6b09fc828..e8f8e8ba6 100644
--- a/package/lib/src/controls/snack_bar.dart
+++ b/package/lib/src/controls/snack_bar.dart
@@ -1,16 +1,12 @@
 import 'package:collection/collection.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/colors.dart';
 import '../utils/edge_insets.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class SnackBarControl extends StatefulWidget {
   final Control? parent;
@@ -31,7 +27,8 @@ class SnackBarControl extends StatefulWidget {
   State<SnackBarControl> createState() => _SnackBarControlState();
 }
 
-class _SnackBarControlState extends State<SnackBarControl> {
+class _SnackBarControlState extends State<SnackBarControl>
+    with FletControlStatefulMixin {
   bool _open = false;
 
   Widget _createSnackBar() {
@@ -50,10 +47,7 @@ class _SnackBarControlState extends State<SnackBarControl> {
                 widget.control.attrString("actionColor", "")!),
             onPressed: () {
               debugPrint("SnackBar ${widget.control.id} clicked!");
-              FletAppServices.of(context).server.sendPageEvent(
-                  eventTarget: widget.control.id,
-                  eventName: "action",
-                  eventData: "");
+              sendControlEvent(widget.control.id, "action", "");
             })
         : null;
 
@@ -89,47 +83,34 @@ class _SnackBarControlState extends State<SnackBarControl> {
   Widget build(BuildContext context) {
     debugPrint("SnackBar build: ${widget.control.id}");
 
-    return StoreConnector<AppState, Function>(
-        distinct: true,
-        converter: (store) => store.dispatch,
-        builder: (context, dispatch) {
-          debugPrint("SnackBar StoreConnector build: ${widget.control.id}");
-
-          var open = widget.control.attrBool("open", false)!;
-          var removeCurrentSnackbar = true;
+    debugPrint("SnackBar build: ${widget.control.id}");
 
-          //widget.control.attrBool("removeCurrentSnackBar", false)!;
+    var open = widget.control.attrBool("open", false)!;
+    var removeCurrentSnackbar = true;
 
-          debugPrint("Current open state: $_open");
-          debugPrint("New open state: $open");
+    //widget.control.attrBool("removeCurrentSnackBar", false)!;
 
-          if (open && (open != _open)) {
-            var snackBar = _createSnackBar();
-            if (snackBar is ErrorControl) {
-              return snackBar;
-            }
+    debugPrint("Current open state: $_open");
+    debugPrint("New open state: $open");
 
-            WidgetsBinding.instance.addPostFrameCallback((_) {
-              if (removeCurrentSnackbar) {
-                ScaffoldMessenger.of(context).removeCurrentSnackBar();
-              }
+    if (open && (open != _open)) {
+      var snackBar = _createSnackBar();
+      if (snackBar is ErrorControl) {
+        return snackBar;
+      }
 
-              ScaffoldMessenger.of(context).showSnackBar(snackBar as SnackBar);
+      WidgetsBinding.instance.addPostFrameCallback((_) {
+        if (removeCurrentSnackbar) {
+          ScaffoldMessenger.of(context).removeCurrentSnackBar();
+        }
+        ScaffoldMessenger.of(context).showSnackBar(snackBar as SnackBar);
 
-              List<Map<String, String>> props = [
-                {"i": widget.control.id, "open": "false"}
-              ];
-              dispatch(UpdateControlPropsAction(
-                  UpdateControlPropsPayload(props: props)));
-              FletAppServices.of(context)
-                  .server
-                  .updateControlProps(props: props);
-            });
-          }
+        updateControlProps(widget.control.id, {"open": "false"});
+      });
+    }
 
-          _open = open;
+    _open = open;
 
-          return widget.nextChild ?? const SizedBox.shrink();
-        });
+    return widget.nextChild ?? const SizedBox.shrink();
   }
 }
diff --git a/package/lib/src/controls/submenu_button.dart b/package/lib/src/controls/submenu_button.dart
index ff2048667..a96483b0f 100644
--- a/package/lib/src/controls/submenu_button.dart
+++ b/package/lib/src/controls/submenu_button.dart
@@ -1,13 +1,11 @@
 import 'package:flutter/material.dart';
 
-import '../flet_app_services.dart';
 import '../models/control.dart';
 import '../utils/buttons.dart';
 import '../utils/menu.dart';
 import '../utils/transforms.dart';
-
 import 'create_control.dart';
-
+import 'flet_control_stateful_mixin.dart';
 
 class SubMenuButtonControl extends StatefulWidget {
   final Control? parent;
@@ -26,7 +24,8 @@ class SubMenuButtonControl extends StatefulWidget {
   State<SubMenuButtonControl> createState() => _SubMenuButtonControlState();
 }
 
-class _SubMenuButtonControlState extends State<SubMenuButtonControl> {
+class _SubMenuButtonControlState extends State<SubMenuButtonControl>
+    with FletControlStatefulMixin {
   late final FocusNode _focusNode;
   String? _lastFocusValue;
 
@@ -45,10 +44,8 @@ class _SubMenuButtonControlState extends State<SubMenuButtonControl> {
   }
 
   void _onFocusChange() {
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   @override
@@ -94,8 +91,6 @@ class _SubMenuButtonControlState extends State<SubMenuButtonControl> {
     bool onClose = widget.control.attrBool("onClose", false)!;
     bool onHover = widget.control.attrBool("onHover", false)!;
 
-    var server = FletAppServices.of(context).server;
-
     var subMenu = SubmenuButton(
       focusNode: _focusNode,
       clipBehavior: clipBehavior,
@@ -106,26 +101,17 @@ class _SubMenuButtonControlState extends State<SubMenuButtonControl> {
           : null,
       onClose: onClose && !disabled
           ? () {
-              server.sendPageEvent(
-                  eventTarget: widget.control.id,
-                  eventName: "close",
-                  eventData: "");
+              sendControlEvent(widget.control.id, "close", "");
             }
           : null,
       onHover: onHover && !disabled
           ? (bool value) {
-              server.sendPageEvent(
-                  eventTarget: widget.control.id,
-                  eventName: "hover",
-                  eventData: "$value");
+              sendControlEvent(widget.control.id, "hover", "$value");
             }
           : null,
       onOpen: onOpen && !disabled
           ? () {
-              server.sendPageEvent(
-                  eventTarget: widget.control.id,
-                  eventName: "open",
-                  eventData: "");
+              sendControlEvent(widget.control.id, "open", "");
             }
           : null,
       leadingIcon: leading.isNotEmpty
diff --git a/package/lib/src/controls/switch.dart b/package/lib/src/controls/switch.dart
index a4f5f0cec..df1cd7b42 100644
--- a/package/lib/src/controls/switch.dart
+++ b/package/lib/src/controls/switch.dart
@@ -1,18 +1,13 @@
-import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
-import '../utils/buttons.dart';
 import '../utils/colors.dart';
 import '../utils/icons.dart';
 import 'create_control.dart';
-import 'list_tile.dart';
 import 'cupertino_switch.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
+import 'list_tile.dart';
 
 enum LabelPosition { right, left }
 
@@ -20,20 +15,19 @@ class SwitchControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const SwitchControl(
       {super.key,
       this.parent,
       required this.control,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<SwitchControl> createState() => _SwitchControlState();
 }
 
-class _SwitchControlState extends State<SwitchControl> {
+class _SwitchControlState extends State<SwitchControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   bool _value = false;
   late final FocusNode _focusNode;
 
@@ -54,112 +48,94 @@ class _SwitchControlState extends State<SwitchControl> {
   void _onChange(bool value) {
     var svalue = value.toString();
     debugPrint(svalue);
-    setState(() {
-      _value = value;
-    });
-    List<Map<String, String>> props = [
-      {"i": widget.control.id, "value": svalue}
-    ];
-    widget.dispatch(
-        UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-    final server = FletAppServices.of(context).server;
-    server.updateControlProps(props: props);
-    server.sendPageEvent(
-        eventTarget: widget.control.id, eventName: "change", eventData: svalue);
+    _value = value;
+    updateControlProps(widget.control.id, {"value": svalue});
+    sendControlEvent(widget.control.id, "change", svalue);
   }
 
   void _onFocusChange() {
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   @override
   Widget build(BuildContext context) {
     debugPrint("SwitchControl build: ${widget.control.id}");
 
-    bool adaptive = widget.control.attrBool("adaptive", false)!;
-    if (adaptive &&
-        (defaultTargetPlatform == TargetPlatform.iOS ||
-            defaultTargetPlatform == TargetPlatform.macOS)) {
-      return CupertinoSwitchControl(
-          control: widget.control,
-          parentDisabled: widget.parentDisabled,
-          dispatch: widget.dispatch);
-    }
-
-    String label = widget.control.attrString("label", "")!;
-    LabelPosition labelPosition = LabelPosition.values.firstWhere(
-        (p) =>
-            p.name.toLowerCase() ==
-            widget.control.attrString("labelPosition", "")!.toLowerCase(),
-        orElse: () => LabelPosition.right);
-    bool autofocus = widget.control.attrBool("autofocus", false)!;
-    bool disabled = widget.control.isDisabled || widget.parentDisabled;
-
-    return StoreConnector<AppState, Function>(
-        distinct: true,
-        converter: (store) => store.dispatch,
-        builder: (context, dispatch) {
-          debugPrint("Switch StoreConnector build: ${widget.control.id}");
-
-          bool value = widget.control.attrBool("value", false)!;
-          if (_value != value) {
-            _value = value;
-          }
-
-          var swtch = Switch(
-              autofocus: autofocus,
-              focusNode: _focusNode,
-              activeColor: HexColor.fromString(Theme.of(context),
-                  widget.control.attrString("activeColor", "")!),
-              activeTrackColor: HexColor.fromString(Theme.of(context),
-                  widget.control.attrString("activeTrackColor", "")!),
-              inactiveThumbColor: HexColor.fromString(Theme.of(context),
-                  widget.control.attrString("inactiveThumbColor", "")!),
-              inactiveTrackColor: HexColor.fromString(Theme.of(context),
-                  widget.control.attrString("inactiveTrackColor", "")!),
-              thumbColor: parseMaterialStateColor(
-                  Theme.of(context), widget.control, "thumbColor"),
-              thumbIcon: parseMaterialStateIcon(
-                  Theme.of(context), widget.control, "thumbIcon"),
-              trackColor: parseMaterialStateColor(
-                  Theme.of(context), widget.control, "trackColor"),
-              focusColor: HexColor.fromString(Theme.of(context),
-                  widget.control.attrString("focusColor", "")!),
-              value: _value,
-              onChanged: !disabled
-                  ? (bool value) {
-                      _onChange(value);
-                    }
-                  : null);
-
-          ListTileClicks.of(context)?.notifier.addListener(() {
-            _onChange(!_value);
-          });
-
-          Widget result = swtch;
-          if (label != "") {
-            var labelWidget = disabled
-                ? Text(label,
-                    style: TextStyle(color: Theme.of(context).disabledColor))
-                : MouseRegion(
-                    cursor: SystemMouseCursors.click, child: Text(label));
-            result = MergeSemantics(
-                child: GestureDetector(
-                    onTap: !disabled
-                        ? () {
-                            _onChange(!_value);
-                          }
-                        : null,
-                    child: labelPosition == LabelPosition.right
-                        ? Row(children: [swtch, labelWidget])
-                        : Row(children: [labelWidget, swtch])));
-          }
-
-          return constrainedControl(
-              context, result, widget.parent, widget.control);
-        });
+    return withPagePlatform((context, platform) {
+      bool adaptive = widget.control.attrBool("adaptive", false)!;
+      if (adaptive &&
+          (platform == TargetPlatform.iOS ||
+              platform == TargetPlatform.macOS)) {
+        return CupertinoSwitchControl(
+            control: widget.control, parentDisabled: widget.parentDisabled);
+      }
+
+      String label = widget.control.attrString("label", "")!;
+      LabelPosition labelPosition = LabelPosition.values.firstWhere(
+          (p) =>
+              p.name.toLowerCase() ==
+              widget.control.attrString("labelPosition", "")!.toLowerCase(),
+          orElse: () => LabelPosition.right);
+      bool autofocus = widget.control.attrBool("autofocus", false)!;
+      bool disabled = widget.control.isDisabled || widget.parentDisabled;
+
+      debugPrint("Switch build: ${widget.control.id}");
+
+      bool value = widget.control.attrBool("value", false)!;
+      if (_value != value) {
+        _value = value;
+      }
+
+      var swtch = Switch(
+          autofocus: autofocus,
+          focusNode: _focusNode,
+          activeColor: HexColor.fromString(
+              Theme.of(context), widget.control.attrString("activeColor", "")!),
+          activeTrackColor: HexColor.fromString(Theme.of(context),
+              widget.control.attrString("activeTrackColor", "")!),
+          inactiveThumbColor: HexColor.fromString(Theme.of(context),
+              widget.control.attrString("inactiveThumbColor", "")!),
+          inactiveTrackColor: HexColor.fromString(Theme.of(context),
+              widget.control.attrString("inactiveTrackColor", "")!),
+          thumbColor: parseMaterialStateColor(
+              Theme.of(context), widget.control, "thumbColor"),
+          thumbIcon: parseMaterialStateIcon(
+              Theme.of(context), widget.control, "thumbIcon"),
+          trackColor: parseMaterialStateColor(
+              Theme.of(context), widget.control, "trackColor"),
+          focusColor: HexColor.fromString(
+              Theme.of(context), widget.control.attrString("focusColor", "")!),
+          value: _value,
+          onChanged: !disabled
+              ? (bool value) {
+                  _onChange(value);
+                }
+              : null);
+
+      ListTileClicks.of(context)?.notifier.addListener(() {
+        _onChange(!_value);
+      });
+
+      Widget result = swtch;
+      if (label != "") {
+        var labelWidget = disabled
+            ? Text(label,
+                style: TextStyle(color: Theme.of(context).disabledColor))
+            : MouseRegion(cursor: SystemMouseCursors.click, child: Text(label));
+        result = MergeSemantics(
+            child: GestureDetector(
+                onTap: !disabled
+                    ? () {
+                        _onChange(!_value);
+                      }
+                    : null,
+                child: labelPosition == LabelPosition.right
+                    ? Row(children: [swtch, labelWidget])
+                    : Row(children: [labelWidget, swtch])));
+      }
+
+      return constrainedControl(context, result, widget.parent, widget.control);
+    });
   }
 }
diff --git a/package/lib/src/controls/tabs.dart b/package/lib/src/controls/tabs.dart
index fb174f580..b09461428 100644
--- a/package/lib/src/controls/tabs.dart
+++ b/package/lib/src/controls/tabs.dart
@@ -1,42 +1,46 @@
 import 'dart:convert';
 
-import 'package:flet/src/utils/alignment.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
 import '../models/app_state.dart';
 import '../models/control.dart';
 import '../models/controls_view_model.dart';
-import '../protocol/update_control_props_payload.dart';
+import '../utils/alignment.dart';
 import '../utils/borders.dart';
 import '../utils/colors.dart';
 import '../utils/edge_insets.dart';
 import '../utils/icons.dart';
 import '../utils/material_state.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class TabsControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final List<Control> children;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const TabsControl(
       {super.key,
       this.parent,
       required this.control,
       required this.children,
-      required this.parentDisabled,
-      required this.dispatch});
+      required this.parentDisabled});
 
   @override
   State<TabsControl> createState() => _TabsControlState();
 }
 
-class _TabsControlState extends State<TabsControl>
+class _TabsControlStateWithControlState extends State<TabsControl>
+    with FletControlStatefulMixin {
+  @override
+  Widget build(BuildContext context) {
+    throw UnimplementedError();
+  }
+}
+
+class _TabsControlState extends _TabsControlStateWithControlState
     with TickerProviderStateMixin {
   String? _tabsSnapshot;
   TabController? _tabController;
@@ -56,17 +60,9 @@ class _TabsControlState extends State<TabsControl>
     var index = _tabController!.index;
     if (_selectedIndex != index) {
       debugPrint("Selected index: $index");
-      List<Map<String, String>> props = [
-        {"i": widget.control.id, "selectedindex": index.toString()}
-      ];
-      widget.dispatch(
-          UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-      final server = FletAppServices.of(context).server;
-      server.updateControlProps(props: props);
-      server.sendPageEvent(
-          eventTarget: widget.control.id,
-          eventName: "change",
-          eventData: index.toString());
+      updateControlProps(
+          widget.control.id, {"selectedindex": index.toString()});
+      sendControlEvent(widget.control.id, "change", index.toString());
       _selectedIndex = index;
     }
   }
@@ -187,8 +183,7 @@ class _TabsControlState extends State<TabsControl>
                   TabBarTheme.of(context).overlayColor,
               tabs: viewModel.controlViews.map((tabView) {
                 var text = tabView.control.attrString("text");
-                var icon =
-                    parseIcon(tabView.control.attrString("icon", "")!);
+                var icon = parseIcon(tabView.control.attrString("icon", "")!);
                 var tabContentCtrls = tabView.children
                     .where((c) => c.name == "tab_content" && c.isVisible);
 
diff --git a/package/lib/src/controls/text.dart b/package/lib/src/controls/text.dart
index 1762ae652..6cce0fd95 100644
--- a/package/lib/src/controls/text.dart
+++ b/package/lib/src/controls/text.dart
@@ -1,18 +1,17 @@
 import 'dart:ui';
 
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/control_tree_view_model.dart';
 import '../utils/colors.dart';
 import '../utils/numbers.dart';
 import '../utils/text.dart';
 import 'create_control.dart';
+import 'flet_control_stateless_mixin.dart';
+import 'flet_store_mixin.dart';
 
-class TextControl extends StatelessWidget {
+class TextControl extends StatelessWidget
+    with FletControlStatelessMixin, FletStoreMixin {
   final Control? parent;
   final Control control;
   final bool parentDisabled;
@@ -26,121 +25,121 @@ class TextControl extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    var result = StoreConnector<AppState, ControlTreeViewModel>(
-        distinct: true,
-        converter: (store) => ControlTreeViewModel.fromStore(store, control),
-        builder: (context, viewModel) {
-          debugPrint("Text build: ${control.id}");
-
-          bool disabled = control.isDisabled || parentDisabled;
-
-          String text = control.attrString("value", "")!;
-          List<InlineSpan>? spans = parseTextSpans(
-            Theme.of(context),
-            viewModel,
-            disabled,
-            FletAppServices.of(context).server,
-          );
-          String? semanticsLabel = control.attrString("semanticsLabel");
-          bool noWrap = control.attrBool("noWrap", false)!;
-          int? maxLines = control.attrInt("maxLines");
-
-          TextStyle? style;
-          var styleNameOrData = control.attrString("style", null);
-          if (styleNameOrData != null) {
-            style = getTextStyle(context, styleNameOrData);
-          }
-          if (style == null && styleNameOrData != null) {
-            try {
-              style = parseTextStyle(Theme.of(context), control, "style");
-            } on FormatException catch (_) {
-              style = null;
-            }
-          }
-
-          TextStyle? themeStyle;
-          var styleName = control.attrString("theme_style", null);
-          if (styleName != null) {
-            themeStyle = getTextStyle(context, styleName);
-          }
-
-          if (style == null && themeStyle != null) {
-            style = themeStyle;
-          } else if (style != null && themeStyle != null) {
-            style = themeStyle.merge(style);
-          }
-
-          var fontWeight = control.attrString("weight", "")!;
-
-          List<FontVariation> variations = [];
-          if (fontWeight.startsWith("w")) {
-            variations.add(
-                FontVariation('wght', parseDouble(fontWeight.substring(1))));
-          }
-
-          style = (style ?? const TextStyle()).copyWith(
-              fontSize: control.attrDouble("size", null),
-              fontWeight: getFontWeight(fontWeight),
-              fontStyle: control.attrBool(
-                "italic",
-                false,
-              )!
-                  ? FontStyle.italic
-                  : null,
-              fontFamily: control.attrString("fontFamily"),
-              fontVariations: variations,
-              color: HexColor.fromString(
-                      Theme.of(context), control.attrString("color", "")!) ??
-                  (spans.isNotEmpty
-                      ? DefaultTextStyle.of(context).style.color
-                      : null),
-              backgroundColor: HexColor.fromString(
-                  Theme.of(context), control.attrString("bgcolor", "")!));
-
-          TextAlign textAlign = TextAlign.values.firstWhere(
-              (a) =>
-                  a.name.toLowerCase() ==
-                  control.attrString("textAlign", "")!.toLowerCase(),
-              orElse: () => TextAlign.start);
-
-          TextOverflow overflow = TextOverflow.values.firstWhere(
-              (v) =>
-                  v.name.toLowerCase() ==
-                  control.attrString("overflow", "")!.toLowerCase(),
-              orElse: () => TextOverflow.clip);
-
-          return control.attrBool("selectable", false)!
-              ? (spans.isNotEmpty)
-                  ? SelectableText.rich(
-                      TextSpan(text: text, style: style, children: spans),
-                      maxLines: maxLines,
-                      textAlign: textAlign,
-                    )
-                  : SelectableText(
-                      text,
-                      semanticsLabel: semanticsLabel,
-                      maxLines: maxLines,
-                      style: style,
-                      textAlign: textAlign,
-                    )
-              : (spans.isNotEmpty)
-                  ? RichText(
-                      text: TextSpan(text: text, style: style, children: spans),
-                      maxLines: maxLines,
-                      softWrap: !noWrap,
-                      textAlign: textAlign,
-                      overflow: overflow,
-                    )
-                  : Text(
-                      text,
-                      semanticsLabel: semanticsLabel,
-                      maxLines: maxLines,
-                      softWrap: !noWrap,
-                      style: style,
-                      textAlign: textAlign,
-                      overflow: overflow,
-                    );
-        });
+    var result = withControlTree(control, (context, viewModel) {
+      debugPrint("Text build: ${control.id}");
+
+      bool disabled = control.isDisabled || parentDisabled;
+
+      String text = control.attrString("value", "")!;
+
+      List<InlineSpan>? spans = parseTextSpans(
+        Theme.of(context),
+        viewModel,
+        disabled,
+        (String controlId, String eventName, String eventData) {
+          sendControlEvent(context, controlId, eventName, eventData);
+        },
+      );
+      String? semanticsLabel = control.attrString("semanticsLabel");
+      bool noWrap = control.attrBool("noWrap", false)!;
+      int? maxLines = control.attrInt("maxLines");
+
+      TextStyle? style;
+      var styleNameOrData = control.attrString("style", null);
+      if (styleNameOrData != null) {
+        style = getTextStyle(context, styleNameOrData);
+      }
+      if (style == null && styleNameOrData != null) {
+        try {
+          style = parseTextStyle(Theme.of(context), control, "style");
+        } on FormatException catch (_) {
+          style = null;
+        }
+      }
+
+      TextStyle? themeStyle;
+      var styleName = control.attrString("theme_style", null);
+      if (styleName != null) {
+        themeStyle = getTextStyle(context, styleName);
+      }
+
+      if (style == null && themeStyle != null) {
+        style = themeStyle;
+      } else if (style != null && themeStyle != null) {
+        style = themeStyle.merge(style);
+      }
+
+      var fontWeight = control.attrString("weight", "")!;
+
+      List<FontVariation> variations = [];
+      if (fontWeight.startsWith("w")) {
+        variations
+            .add(FontVariation('wght', parseDouble(fontWeight.substring(1))));
+      }
+
+      style = (style ?? const TextStyle()).copyWith(
+          fontSize: control.attrDouble("size", null),
+          fontWeight: getFontWeight(fontWeight),
+          fontStyle: control.attrBool(
+            "italic",
+            false,
+          )!
+              ? FontStyle.italic
+              : null,
+          fontFamily: control.attrString("fontFamily"),
+          fontVariations: variations,
+          color: HexColor.fromString(
+                  Theme.of(context), control.attrString("color", "")!) ??
+              (spans.isNotEmpty
+                  ? DefaultTextStyle.of(context).style.color
+                  : null),
+          backgroundColor: HexColor.fromString(
+              Theme.of(context), control.attrString("bgcolor", "")!));
+
+      TextAlign textAlign = TextAlign.values.firstWhere(
+          (a) =>
+              a.name.toLowerCase() ==
+              control.attrString("textAlign", "")!.toLowerCase(),
+          orElse: () => TextAlign.start);
+
+      TextOverflow overflow = TextOverflow.values.firstWhere(
+          (v) =>
+              v.name.toLowerCase() ==
+              control.attrString("overflow", "")!.toLowerCase(),
+          orElse: () => TextOverflow.clip);
+
+      return control.attrBool("selectable", false)!
+          ? (spans.isNotEmpty)
+              ? SelectableText.rich(
+                  TextSpan(text: text, style: style, children: spans),
+                  maxLines: maxLines,
+                  textAlign: textAlign,
+                )
+              : SelectableText(
+                  text,
+                  semanticsLabel: semanticsLabel,
+                  maxLines: maxLines,
+                  style: style,
+                  textAlign: textAlign,
+                )
+          : (spans.isNotEmpty)
+              ? RichText(
+                  text: TextSpan(text: text, style: style, children: spans),
+                  maxLines: maxLines,
+                  softWrap: !noWrap,
+                  textAlign: textAlign,
+                  overflow: overflow,
+                )
+              : Text(
+                  text,
+                  semanticsLabel: semanticsLabel,
+                  maxLines: maxLines,
+                  softWrap: !noWrap,
+                  style: style,
+                  textAlign: textAlign,
+                  overflow: overflow,
+                );
+    });
 
     return constrainedControl(context, result, parent, control);
   }
diff --git a/package/lib/src/controls/text_button.dart b/package/lib/src/controls/text_button.dart
index c3e03af60..5ac1fab31 100644
--- a/package/lib/src/controls/text_button.dart
+++ b/package/lib/src/controls/text_button.dart
@@ -1,12 +1,12 @@
 import 'package:flutter/material.dart';
 
-import '../flet_app_services.dart';
 import '../models/control.dart';
 import '../utils/buttons.dart';
 import '../utils/colors.dart';
 import '../utils/icons.dart';
 import '../utils/launch_url.dart';
 import 'create_control.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class TextButtonControl extends StatefulWidget {
   final Control? parent;
@@ -25,7 +25,8 @@ class TextButtonControl extends StatefulWidget {
   State<TextButtonControl> createState() => _TextButtonControlState();
 }
 
-class _TextButtonControlState extends State<TextButtonControl> {
+class _TextButtonControlState extends State<TextButtonControl>
+    with FletControlStatefulMixin {
   late final FocusNode _focusNode;
   String? _lastFocusValue;
 
@@ -44,18 +45,14 @@ class _TextButtonControlState extends State<TextButtonControl> {
   }
 
   void _onFocusChange() {
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   @override
   Widget build(BuildContext context) {
     debugPrint("Button build: ${widget.control.id}");
 
-    final server = FletAppServices.of(context).server;
-
     String text = widget.control.attrString("text", "")!;
     IconData? icon = parseIcon(widget.control.attrString("icon", "")!);
     Color? iconColor = HexColor.fromString(
@@ -74,30 +71,21 @@ class _TextButtonControlState extends State<TextButtonControl> {
             if (url != "") {
               openWebBrowser(url, webWindowName: urlTarget);
             }
-            server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "click",
-                eventData: "");
+            sendControlEvent(widget.control.id, "click", "");
           }
         : null;
 
     Function()? onLongPressHandler = onLongPress && !disabled
         ? () {
             debugPrint("Button ${widget.control.id} long pressed!");
-            server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "long_press",
-                eventData: "");
+            sendControlEvent(widget.control.id, "long_press", "");
           }
         : null;
 
     Function(bool)? onHoverHandler = onHover && !disabled
         ? (state) {
             debugPrint("Button ${widget.control.id} hovered!");
-            server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "hover",
-                eventData: state.toString());
+            sendControlEvent(widget.control.id, "hover", state.toString());
           }
         : null;
 
diff --git a/package/lib/src/controls/textfield.dart b/package/lib/src/controls/textfield.dart
index 513efc90f..201066a8c 100644
--- a/package/lib/src/controls/textfield.dart
+++ b/package/lib/src/controls/textfield.dart
@@ -1,19 +1,15 @@
-import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
 import '../utils/borders.dart';
 import '../utils/colors.dart';
 import '../utils/text.dart';
 import '../utils/textfield.dart';
 import 'create_control.dart';
 import 'cupertino_textfield.dart';
+import 'flet_control_stateful_mixin.dart';
+import 'flet_store_mixin.dart';
 import 'form_field.dart';
 
 class TextFieldControl extends StatefulWidget {
@@ -33,7 +29,8 @@ class TextFieldControl extends StatefulWidget {
   State<TextFieldControl> createState() => _TextFieldControlState();
 }
 
-class _TextFieldControlState extends State<TextFieldControl> {
+class _TextFieldControlState extends State<TextFieldControl>
+    with FletControlStatefulMixin, FletStoreMixin {
   String _value = "";
   bool _revealPassword = false;
   bool _focused = false;
@@ -50,10 +47,7 @@ class _TextFieldControlState extends State<TextFieldControl> {
       onKey: (FocusNode node, RawKeyEvent evt) {
         if (!evt.isShiftPressed && evt.logicalKey.keyLabel == 'Enter') {
           if (evt is RawKeyDownEvent) {
-            FletAppServices.of(context).server.sendPageEvent(
-                eventTarget: widget.control.id,
-                eventName: "submit",
-                eventData: "");
+            sendControlEvent(widget.control.id, "submit", "");
           }
           return KeyEventResult.handled;
         } else {
@@ -80,247 +74,220 @@ class _TextFieldControlState extends State<TextFieldControl> {
     setState(() {
       _focused = _shiftEnterfocusNode.hasFocus;
     });
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _shiftEnterfocusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(widget.control.id,
+        _shiftEnterfocusNode.hasFocus ? "focus" : "blur", "");
   }
 
   void _onFocusChange() {
     setState(() {
       _focused = _focusNode.hasFocus;
     });
-    FletAppServices.of(context).server.sendPageEvent(
-        eventTarget: widget.control.id,
-        eventName: _focusNode.hasFocus ? "focus" : "blur",
-        eventData: "");
+    sendControlEvent(
+        widget.control.id, _focusNode.hasFocus ? "focus" : "blur", "");
   }
 
   @override
   Widget build(BuildContext context) {
     debugPrint("TextField build: ${widget.control.id}");
 
-    bool autofocus = widget.control.attrBool("autofocus", false)!;
-    bool disabled = widget.control.isDisabled || widget.parentDisabled;
-
-    bool adaptive = widget.control.attrBool("adaptive", false)!;
-    if (adaptive &&
-        (defaultTargetPlatform == TargetPlatform.iOS ||
-            defaultTargetPlatform == TargetPlatform.macOS)) {
-      return CupertinoTextFieldControl(
-          control: widget.control,
-          children: widget.children,
-          parent: widget.parent,
-          parentDisabled: widget.parentDisabled);
-    }
+    return withPagePlatform((context, platform) {
+      bool autofocus = widget.control.attrBool("autofocus", false)!;
+      bool disabled = widget.control.isDisabled || widget.parentDisabled;
+
+      bool adaptive = widget.control.attrBool("adaptive", false)!;
+      if (adaptive &&
+          (platform == TargetPlatform.iOS ||
+              platform == TargetPlatform.macOS)) {
+        return CupertinoTextFieldControl(
+            control: widget.control,
+            children: widget.children,
+            parent: widget.parent,
+            parentDisabled: widget.parentDisabled);
+      }
 
-    return StoreConnector<AppState, Function>(
-        distinct: true,
-        converter: (store) => store.dispatch,
-        builder: (context, dispatch) {
-          debugPrint("TextField StoreConnector build: ${widget.control.id}");
+      debugPrint("TextField build: ${widget.control.id}");
 
-          String value = widget.control.attrs["value"] ?? "";
-          if (_value != value) {
-            _value = value;
-            _controller.text = value;
-          }
+      String value = widget.control.attrs["value"] ?? "";
+      if (_value != value) {
+        _value = value;
+        _controller.text = value;
+      }
 
-          var prefixControls =
-              widget.children.where((c) => c.name == "prefix" && c.isVisible);
-          var suffixControls =
-              widget.children.where((c) => c.name == "suffix" && c.isVisible);
-
-          bool shiftEnter = widget.control.attrBool("shiftEnter", false)!;
-          bool multiline =
-              widget.control.attrBool("multiline", false)! || shiftEnter;
-          int minLines = widget.control.attrInt("minLines", 1)!;
-          int? maxLines =
-              widget.control.attrInt("maxLines", multiline ? null : 1);
-
-          bool readOnly = widget.control.attrBool("readOnly", false)!;
-          bool password = widget.control.attrBool("password", false)!;
-          bool canRevealPassword =
-              widget.control.attrBool("canRevealPassword", false)!;
-          bool onChange = widget.control.attrBool("onChange", false)!;
-
-          var cursorColor = HexColor.fromString(
-              Theme.of(context), widget.control.attrString("cursorColor", "")!);
-          var selectionColor = HexColor.fromString(Theme.of(context),
-              widget.control.attrString("selectionColor", "")!);
-
-          int? maxLength = widget.control.attrInt("maxLength");
-
-          var textSize = widget.control.attrDouble("textSize");
-
-          var color = HexColor.fromString(
-              Theme.of(context), widget.control.attrString("color", "")!);
-          var focusedColor = HexColor.fromString(Theme.of(context),
-              widget.control.attrString("focusedColor", "")!);
-
-          TextStyle? textStyle =
-              parseTextStyle(Theme.of(context), widget.control, "textStyle");
-          if (textSize != null || color != null || focusedColor != null) {
-            textStyle = (textStyle ?? const TextStyle()).copyWith(
-                fontSize: textSize,
-                color: _focused ? focusedColor ?? color : color);
-          }
+      var prefixControls =
+          widget.children.where((c) => c.name == "prefix" && c.isVisible);
+      var suffixControls =
+          widget.children.where((c) => c.name == "suffix" && c.isVisible);
+
+      bool shiftEnter = widget.control.attrBool("shiftEnter", false)!;
+      bool multiline =
+          widget.control.attrBool("multiline", false)! || shiftEnter;
+      int minLines = widget.control.attrInt("minLines", 1)!;
+      int? maxLines = widget.control.attrInt("maxLines", multiline ? null : 1);
+
+      bool readOnly = widget.control.attrBool("readOnly", false)!;
+      bool password = widget.control.attrBool("password", false)!;
+      bool canRevealPassword =
+          widget.control.attrBool("canRevealPassword", false)!;
+      bool onChange = widget.control.attrBool("onChange", false)!;
+
+      var cursorColor = HexColor.fromString(
+          Theme.of(context), widget.control.attrString("cursorColor", "")!);
+      var selectionColor = HexColor.fromString(
+          Theme.of(context), widget.control.attrString("selectionColor", "")!);
+
+      int? maxLength = widget.control.attrInt("maxLength");
+
+      var textSize = widget.control.attrDouble("textSize");
+
+      var color = HexColor.fromString(
+          Theme.of(context), widget.control.attrString("color", "")!);
+      var focusedColor = HexColor.fromString(
+          Theme.of(context), widget.control.attrString("focusedColor", "")!);
+
+      TextStyle? textStyle =
+          parseTextStyle(Theme.of(context), widget.control, "textStyle");
+      if (textSize != null || color != null || focusedColor != null) {
+        textStyle = (textStyle ?? const TextStyle()).copyWith(
+            fontSize: textSize,
+            color: _focused ? focusedColor ?? color : color);
+      }
 
-          TextCapitalization? textCapitalization = TextCapitalization.values
-              .firstWhere(
-                  (a) =>
-                      a.name.toLowerCase() ==
-                      widget.control
-                          .attrString("capitalization", "")!
-                          .toLowerCase(),
-                  orElse: () => TextCapitalization.none);
-
-          FilteringTextInputFormatter? inputFilter =
-              parseInputFilter(widget.control, "inputFilter");
-
-          List<TextInputFormatter>? inputFormatters = [];
-          // add non-null input formatters
-          if (inputFilter != null) {
-            inputFormatters.add(inputFilter);
-          }
-          if (textCapitalization != TextCapitalization.none) {
-            inputFormatters
-                .add(TextCapitalizationFormatter(textCapitalization));
-          }
+      TextCapitalization? textCapitalization = TextCapitalization.values
+          .firstWhere(
+              (a) =>
+                  a.name.toLowerCase() ==
+                  widget.control
+                      .attrString("capitalization", "")!
+                      .toLowerCase(),
+              orElse: () => TextCapitalization.none);
+
+      FilteringTextInputFormatter? inputFilter =
+          parseInputFilter(widget.control, "inputFilter");
+
+      List<TextInputFormatter>? inputFormatters = [];
+      // add non-null input formatters
+      if (inputFilter != null) {
+        inputFormatters.add(inputFilter);
+      }
+      if (textCapitalization != TextCapitalization.none) {
+        inputFormatters.add(TextCapitalizationFormatter(textCapitalization));
+      }
 
-          Widget? revealPasswordIcon;
-          if (password && canRevealPassword) {
-            revealPasswordIcon = GestureDetector(
-                child: Icon(
-                  _revealPassword ? Icons.visibility_off : Icons.visibility,
-                ),
-                onTap: () {
-                  setState(() {
-                    _revealPassword = !_revealPassword;
-                  });
-                });
-          }
+      Widget? revealPasswordIcon;
+      if (password && canRevealPassword) {
+        revealPasswordIcon = GestureDetector(
+            child: Icon(
+              _revealPassword ? Icons.visibility_off : Icons.visibility,
+            ),
+            onTap: () {
+              setState(() {
+                _revealPassword = !_revealPassword;
+              });
+            });
+      }
 
-          TextInputType keyboardType = parseTextInputType(
-              widget.control.attrString("keyboardType", "")!);
+      TextInputType keyboardType =
+          parseTextInputType(widget.control.attrString("keyboardType", "")!);
 
-          if (multiline) {
-            keyboardType = TextInputType.multiline;
-          }
+      if (multiline) {
+        keyboardType = TextInputType.multiline;
+      }
 
-          TextAlign textAlign = TextAlign.values.firstWhere(
-            ((b) =>
-                b.name ==
-                widget.control.attrString("textAlign", "")!.toLowerCase()),
-            orElse: () => TextAlign.start,
-          );
-
-          bool autocorrect = widget.control.attrBool("autocorrect", true)!;
-          bool enableSuggestions =
-              widget.control.attrBool("enableSuggestions", true)!;
-          bool smartDashesType =
-              widget.control.attrBool("smartDashesType", true)!;
-          bool smartQuotesType =
-              widget.control.attrBool("smartQuotesType", true)!;
-
-          FocusNode focusNode = shiftEnter ? _shiftEnterfocusNode : _focusNode;
-
-          var focusValue = widget.control.attrString("focus");
-          if (focusValue != null && focusValue != _lastFocusValue) {
-            _lastFocusValue = focusValue;
-            focusNode.requestFocus();
-          }
+      TextAlign textAlign = TextAlign.values.firstWhere(
+        ((b) =>
+            b.name ==
+            widget.control.attrString("textAlign", "")!.toLowerCase()),
+        orElse: () => TextAlign.start,
+      );
+
+      bool autocorrect = widget.control.attrBool("autocorrect", true)!;
+      bool enableSuggestions =
+          widget.control.attrBool("enableSuggestions", true)!;
+      bool smartDashesType = widget.control.attrBool("smartDashesType", true)!;
+      bool smartQuotesType = widget.control.attrBool("smartQuotesType", true)!;
+
+      FocusNode focusNode = shiftEnter ? _shiftEnterfocusNode : _focusNode;
+
+      var focusValue = widget.control.attrString("focus");
+      if (focusValue != null && focusValue != _lastFocusValue) {
+        _lastFocusValue = focusValue;
+        focusNode.requestFocus();
+      }
 
-          Widget textField = TextFormField(
-              style: textStyle,
-              autofocus: autofocus,
-              enabled: !disabled,
-              onFieldSubmitted: !multiline
-                  ? (_) {
-                      FletAppServices.of(context).server.sendPageEvent(
-                          eventTarget: widget.control.id,
-                          eventName: "submit",
-                          eventData: "");
-                    }
-                  : null,
-              decoration: buildInputDecoration(
-                  context,
-                  widget.control,
-                  prefixControls.isNotEmpty ? prefixControls.first : null,
-                  suffixControls.isNotEmpty ? suffixControls.first : null,
-                  revealPasswordIcon,
-                  _focused),
-              showCursor: widget.control.attrBool("showCursor"),
-              cursorHeight: widget.control.attrDouble("cursorHeight"),
-              cursorWidth: widget.control.attrDouble("cursorWidth") ?? 2.0,
-              cursorRadius: parseRadius(widget.control, "cursorRadius"),
-              keyboardType: keyboardType,
-              autocorrect: autocorrect,
-              enableSuggestions: enableSuggestions,
-              smartDashesType: smartDashesType
-                  ? SmartDashesType.enabled
-                  : SmartDashesType.disabled,
-              smartQuotesType: smartQuotesType
-                  ? SmartQuotesType.enabled
-                  : SmartQuotesType.disabled,
-              textAlign: textAlign,
-              minLines: minLines,
-              maxLines: maxLines,
-              maxLength: maxLength,
-              readOnly: readOnly,
-              inputFormatters:
-                  inputFormatters.isNotEmpty ? inputFormatters : null,
-              obscureText: password && !_revealPassword,
-              controller: _controller,
-              focusNode: focusNode,
-              onChanged: (String value) {
-                //debugPrint(value);
-                setState(() {
-                  _value = value;
-                });
-                List<Map<String, String>> props = [
-                  {"i": widget.control.id, "value": value}
-                ];
-                dispatch(UpdateControlPropsAction(
-                    UpdateControlPropsPayload(props: props)));
-                FletAppServices.of(context)
-                    .server
-                    .updateControlProps(props: props);
-                if (onChange) {
-                  FletAppServices.of(context).server.sendPageEvent(
-                      eventTarget: widget.control.id,
-                      eventName: "change",
-                      eventData: value);
+      Widget textField = TextFormField(
+          style: textStyle,
+          autofocus: autofocus,
+          enabled: !disabled,
+          onFieldSubmitted: !multiline
+              ? (_) {
+                  sendControlEvent(widget.control.id, "submit", "");
                 }
-              });
+              : null,
+          decoration: buildInputDecoration(
+              context,
+              widget.control,
+              prefixControls.isNotEmpty ? prefixControls.first : null,
+              suffixControls.isNotEmpty ? suffixControls.first : null,
+              revealPasswordIcon,
+              _focused),
+          showCursor: widget.control.attrBool("showCursor"),
+          cursorHeight: widget.control.attrDouble("cursorHeight"),
+          cursorWidth: widget.control.attrDouble("cursorWidth") ?? 2.0,
+          cursorRadius: parseRadius(widget.control, "cursorRadius"),
+          keyboardType: keyboardType,
+          autocorrect: autocorrect,
+          enableSuggestions: enableSuggestions,
+          smartDashesType: smartDashesType
+              ? SmartDashesType.enabled
+              : SmartDashesType.disabled,
+          smartQuotesType: smartQuotesType
+              ? SmartQuotesType.enabled
+              : SmartQuotesType.disabled,
+          textAlign: textAlign,
+          minLines: minLines,
+          maxLines: maxLines,
+          maxLength: maxLength,
+          readOnly: readOnly,
+          inputFormatters: inputFormatters.isNotEmpty ? inputFormatters : null,
+          obscureText: password && !_revealPassword,
+          controller: _controller,
+          focusNode: focusNode,
+          onChanged: (String value) {
+            //debugPrint(value);
+            _value = value;
+            updateControlProps(widget.control.id, {"value": value});
+            if (onChange) {
+              sendControlEvent(widget.control.id, "change", value);
+            }
+          });
+
+      if (cursorColor != null || selectionColor != null) {
+        textField = TextSelectionTheme(
+            data: TextSelectionTheme.of(context).copyWith(
+                cursorColor: cursorColor, selectionColor: selectionColor),
+            child: textField);
+      }
 
-          if (cursorColor != null || selectionColor != null) {
-            textField = TextSelectionTheme(
-                data: TextSelectionTheme.of(context).copyWith(
-                    cursorColor: cursorColor, selectionColor: selectionColor),
-                child: textField);
-          }
+      if (widget.control.attrInt("expand", 0)! > 0) {
+        return constrainedControl(
+            context, textField, widget.parent, widget.control);
+      } else {
+        return LayoutBuilder(
+          builder: (BuildContext context, BoxConstraints constraints) {
+            if (constraints.maxWidth == double.infinity &&
+                widget.control.attrDouble("width") == null) {
+              textField = ConstrainedBox(
+                constraints: const BoxConstraints.tightFor(width: 300),
+                child: textField,
+              );
+            }
 
-          if (widget.control.attrInt("expand", 0)! > 0) {
             return constrainedControl(
                 context, textField, widget.parent, widget.control);
-          } else {
-            return LayoutBuilder(
-              builder: (BuildContext context, BoxConstraints constraints) {
-                if (constraints.maxWidth == double.infinity &&
-                    widget.control.attrDouble("width") == null) {
-                  textField = ConstrainedBox(
-                    constraints: const BoxConstraints.tightFor(width: 300),
-                    child: textField,
-                  );
-                }
-
-                return constrainedControl(
-                    context, textField, widget.parent, widget.control);
-              },
-            );
-          }
-        });
+          },
+        );
+      }
+    });
   }
 }
 
diff --git a/package/lib/src/controls/time_picker.dart b/package/lib/src/controls/time_picker.dart
index 5d612b15b..dadad9a77 100644
--- a/package/lib/src/controls/time_picker.dart
+++ b/package/lib/src/controls/time_picker.dart
@@ -1,16 +1,13 @@
 import 'package:flutter/material.dart';
 
-import '../actions.dart';
-import '../flet_app_services.dart';
 import '../models/control.dart';
-import '../protocol/update_control_props_payload.dart';
+import 'flet_control_stateful_mixin.dart';
 
 class TimePickerControl extends StatefulWidget {
   final Control? parent;
   final Control control;
   final List<Control> children;
   final bool parentDisabled;
-  final dynamic dispatch;
 
   const TimePickerControl({
     super.key,
@@ -18,14 +15,14 @@ class TimePickerControl extends StatefulWidget {
     required this.control,
     required this.children,
     required this.parentDisabled,
-    required this.dispatch,
   });
 
   @override
   State<TimePickerControl> createState() => _TimePickerControlState();
 }
 
-class _TimePickerControlState extends State<TimePickerControl> {
+class _TimePickerControlState extends State<TimePickerControl>
+    with FletControlStatefulMixin {
   @override
   Widget build(BuildContext context) {
     debugPrint("TimePicker build: ${widget.control.id}");
@@ -64,17 +61,9 @@ class _TimePickerControlState extends State<TimePickerControl> {
         eventName = "change";
       }
       widget.control.state["open"] = false;
-      List<Map<String, String>> props = [
-        {"i": widget.control.id, "value": stringValue, "open": "false"}
-      ];
-      widget.dispatch(
-          UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
-      FletAppServices.of(context).server.updateControlProps(props: props);
-
-      FletAppServices.of(context).server.sendPageEvent(
-          eventTarget: widget.control.id,
-          eventName: eventName,
-          eventData: stringValue);
+      updateControlProps(
+          widget.control.id, {"value": stringValue, "open": "false"});
+      sendControlEvent(widget.control.id, eventName, stringValue);
     }
 
     Widget createSelectTimeDialog() {
diff --git a/package/lib/src/controls/webview.dart b/package/lib/src/controls/webview.dart
index 8dc45386d..434616bc1 100644
--- a/package/lib/src/controls/webview.dart
+++ b/package/lib/src/controls/webview.dart
@@ -2,18 +2,15 @@ import 'dart:io' show Platform;
 import 'dart:io';
 
 import 'package:flutter/material.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 import 'package:webview_flutter/webview_flutter.dart';
 
-import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../models/control.dart';
-import '../models/control_tree_view_model.dart';
 import '../utils/colors.dart';
 import 'create_control.dart';
 import 'error.dart';
+import 'flet_control_stateless_mixin.dart';
 
-class WebViewControl extends StatelessWidget {
+class WebViewControl extends StatelessWidget with FletControlStatelessMixin {
   final Control? parent;
   final Control control;
   final bool parentDisabled;
@@ -26,68 +23,55 @@ class WebViewControl extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    var result = StoreConnector<AppState, ControlTreeViewModel>(
-        distinct: true,
-        converter: (store) => ControlTreeViewModel.fromStore(store, control),
-        builder: (context, viewModel) {
-          debugPrint("WebViewControl build: ${control.id}");
+    debugPrint("WebViewControl build: ${control.id}");
 
-          String url = control.attrString("url", "")!;
-          if (url == "") {
-            return const ErrorControl("WebView.url cannot be empty.");
-          }
+    Widget? result;
 
-          bool javascriptEnabled =
-              control.attrBool("javascriptEnabled", false)!;
-          var bgcolor = HexColor.fromString(
-              Theme.of(context), control.attrString("bgcolor", "")!);
-          String preventLink = control.attrString("preventLink", "")!;
+    String url = control.attrString("url", "")!;
+    if (url == "") {
+      return const ErrorControl("WebView.url cannot be empty.");
+    }
 
-          if (Platform.isIOS || Platform.isAndroid) {
-            var controller = WebViewController()
-              ..setJavaScriptMode(javascriptEnabled
-                  ? JavaScriptMode.unrestricted
-                  : JavaScriptMode.disabled)
-              ..setNavigationDelegate(
-                NavigationDelegate(
-                  onProgress: (int progress) {},
-                  onPageStarted: (String url) {
-                    FletAppServices.of(context).server.sendPageEvent(
-                        eventTarget: control.id,
-                        eventName: "page_started",
-                        eventData: url);
-                  },
-                  onPageFinished: (String url) {
-                    FletAppServices.of(context).server.sendPageEvent(
-                        eventTarget: control.id,
-                        eventName: "page_ended",
-                        eventData: url);
-                  },
-                  onWebResourceError: (WebResourceError error) {
-                    FletAppServices.of(context).server.sendPageEvent(
-                        eventTarget: control.id,
-                        eventName: "web_resource_error",
-                        eventData: error.toString());
-                  },
-                  onNavigationRequest: (NavigationRequest request) {
-                    if (preventLink != "" &&
-                        request.url.startsWith(preventLink)) {
-                      return NavigationDecision.prevent;
-                    }
-                    return NavigationDecision.navigate;
-                  },
-                ),
-              );
-            if (bgcolor != null) {
-              controller.setBackgroundColor(bgcolor);
-            }
-            controller.loadRequest(Uri.parse(url));
-            return WebViewWidget(controller: controller);
-          } else {
-            return const ErrorControl(
-                "WebView control is not supported on this platform yet.");
-          }
-        });
+    bool javascriptEnabled = control.attrBool("javascriptEnabled", false)!;
+    var bgcolor = HexColor.fromString(
+        Theme.of(context), control.attrString("bgcolor", "")!);
+    String preventLink = control.attrString("preventLink", "")!;
+
+    if (Platform.isIOS || Platform.isAndroid) {
+      var controller = WebViewController()
+        ..setJavaScriptMode(javascriptEnabled
+            ? JavaScriptMode.unrestricted
+            : JavaScriptMode.disabled)
+        ..setNavigationDelegate(
+          NavigationDelegate(
+            onProgress: (int progress) {},
+            onPageStarted: (String url) {
+              sendControlEvent(context, control.id, "page_started", url);
+            },
+            onPageFinished: (String url) {
+              sendControlEvent(context, control.id, "page_ended", url);
+            },
+            onWebResourceError: (WebResourceError error) {
+              sendControlEvent(
+                  context, control.id, "web_resource_error", error.toString());
+            },
+            onNavigationRequest: (NavigationRequest request) {
+              if (preventLink != "" && request.url.startsWith(preventLink)) {
+                return NavigationDecision.prevent;
+              }
+              return NavigationDecision.navigate;
+            },
+          ),
+        );
+      if (bgcolor != null) {
+        controller.setBackgroundColor(bgcolor);
+      }
+      controller.loadRequest(Uri.parse(url));
+      result = WebViewWidget(controller: controller);
+    } else {
+      result = const ErrorControl(
+          "WebView control is not supported on this platform yet.");
+    }
 
     return constrainedControl(context, result, parent, control);
   }
diff --git a/package/lib/src/flet_app.dart b/package/lib/src/flet_app.dart
index 7d59716fe..8e1aa6b91 100644
--- a/package/lib/src/flet_app.dart
+++ b/package/lib/src/flet_app.dart
@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 
+import 'control_factory.dart';
 import 'flet_app_errors_handler.dart';
 import 'flet_app_main.dart';
 import 'flet_app_services.dart';
@@ -13,6 +14,7 @@ class FletApp extends StatefulWidget {
   final FletAppErrorsHandler? errorsHandler;
   final int? reconnectIntervalMs;
   final int? reconnectTimeoutMs;
+  final List<CreateControlFactory>? createControlFactories;
 
   const FletApp(
       {super.key,
@@ -23,7 +25,8 @@ class FletApp extends StatefulWidget {
       this.title,
       this.errorsHandler,
       this.reconnectIntervalMs,
-      this.reconnectTimeoutMs});
+      this.reconnectTimeoutMs,
+      this.createControlFactories});
 
   @override
   State<FletApp> createState() => _FletAppState();
@@ -52,6 +55,7 @@ class _FletAppState extends State<FletApp> {
           pageUrl: widget.pageUrl,
           assetsDir: widget.assetsDir,
           errorsHandler: widget.errorsHandler,
+          createControlFactories: widget.createControlFactories ?? [],
           child: FletAppMain(title: widget.title ?? "Flet"));
     }
     return _appServices!;
diff --git a/package/lib/src/flet_app_services.dart b/package/lib/src/flet_app_services.dart
index 2e85a33ec..6081b3ca2 100644
--- a/package/lib/src/flet_app_services.dart
+++ b/package/lib/src/flet_app_services.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
 import 'package:redux/redux.dart';
 
 import 'actions.dart';
+import 'control_factory.dart';
 import 'flet_app_errors_handler.dart';
 import 'flet_server.dart';
 import 'flet_server_protocol.dart';
@@ -21,6 +22,7 @@ class FletAppServices extends InheritedWidget {
   late final Store<AppState> store;
   final Map<String, GlobalKey> globalKeys = {};
   final Map<String, ControlInvokeMethodCallback> controlInvokeMethods = {};
+  final List<CreateControlFactory> createControlFactories;
 
   FletAppServices(
       {super.key,
@@ -32,7 +34,8 @@ class FletAppServices extends InheritedWidget {
       this.hideLoadingPage,
       this.controlId,
       this.reconnectIntervalMs,
-      this.reconnectTimeoutMs}) {
+      this.reconnectTimeoutMs,
+      required this.createControlFactories}) {
     store = Store<AppState>(appReducer, initialState: AppState.initial());
     server = FletServer(store, controlInvokeMethods,
         reconnectIntervalMs: reconnectIntervalMs,
diff --git a/package/lib/src/models/barchart_event_data.dart b/package/lib/src/models/barchart_event_data.dart
deleted file mode 100644
index f438069e1..000000000
--- a/package/lib/src/models/barchart_event_data.dart
+++ /dev/null
@@ -1,24 +0,0 @@
-import 'package:equatable/equatable.dart';
-
-class BarChartEventData extends Equatable {
-  final String eventType;
-  final int? groupIndex;
-  final int? rodIndex;
-  final int? stackItemIndex;
-
-  const BarChartEventData(
-      {required this.eventType,
-      required this.groupIndex,
-      required this.rodIndex,
-      required this.stackItemIndex});
-
-  Map<String, dynamic> toJson() => <String, dynamic>{
-        'type': eventType,
-        'group_index': groupIndex,
-        'rod_index': rodIndex,
-        'stack_item_index': stackItemIndex
-      };
-
-  @override
-  List<Object?> get props => [eventType, groupIndex, rodIndex, stackItemIndex];
-}
diff --git a/package/lib/src/models/barchart_group_view_model.dart b/package/lib/src/models/barchart_group_view_model.dart
deleted file mode 100644
index 9358b4bcc..000000000
--- a/package/lib/src/models/barchart_group_view_model.dart
+++ /dev/null
@@ -1,29 +0,0 @@
-import 'package:collection/collection.dart';
-import 'package:equatable/equatable.dart';
-import 'package:redux/redux.dart';
-
-import 'app_state.dart';
-import 'barchart_rod_view_model.dart';
-import 'control.dart';
-
-class BarChartGroupViewModel extends Equatable {
-  final Control control;
-  final List<BarChartRodViewModel> barRods;
-
-  const BarChartGroupViewModel({required this.control, required this.barRods});
-
-  static BarChartGroupViewModel fromStore(
-      Store<AppState> store, Control control) {
-    return BarChartGroupViewModel(
-        control: control,
-        barRods: store.state.controls[control.id]!.childIds
-            .map((childId) => store.state.controls[childId])
-            .whereNotNull()
-            .where((c) => c.isVisible)
-            .map((c) => BarChartRodViewModel.fromStore(store, c))
-            .toList());
-  }
-
-  @override
-  List<Object?> get props => [control, barRods];
-}
diff --git a/package/lib/src/models/barchart_rod_stack_item_view_model.dart b/package/lib/src/models/barchart_rod_stack_item_view_model.dart
deleted file mode 100644
index 4b31084f1..000000000
--- a/package/lib/src/models/barchart_rod_stack_item_view_model.dart
+++ /dev/null
@@ -1,19 +0,0 @@
-import 'package:equatable/equatable.dart';
-import 'package:redux/redux.dart';
-
-import 'app_state.dart';
-import 'control.dart';
-
-class BarChartRodStackItemViewModel extends Equatable {
-  final Control control;
-
-  const BarChartRodStackItemViewModel({required this.control});
-
-  static BarChartRodStackItemViewModel fromStore(
-      Store<AppState> store, Control control) {
-    return BarChartRodStackItemViewModel(control: control);
-  }
-
-  @override
-  List<Object?> get props => [control];
-}
diff --git a/package/lib/src/models/barchart_rod_view_model.dart b/package/lib/src/models/barchart_rod_view_model.dart
deleted file mode 100644
index 15cf92149..000000000
--- a/package/lib/src/models/barchart_rod_view_model.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-import 'package:collection/collection.dart';
-import 'package:equatable/equatable.dart';
-import 'package:redux/redux.dart';
-
-import 'app_state.dart';
-import 'barchart_rod_stack_item_view_model.dart';
-import 'control.dart';
-
-class BarChartRodViewModel extends Equatable {
-  final Control control;
-  final List<BarChartRodStackItemViewModel> rodStackItems;
-
-  const BarChartRodViewModel(
-      {required this.control, required this.rodStackItems});
-
-  static BarChartRodViewModel fromStore(
-      Store<AppState> store, Control control) {
-    return BarChartRodViewModel(
-        control: control,
-        rodStackItems: store.state.controls[control.id]!.childIds
-            .map((childId) => store.state.controls[childId])
-            .whereNotNull()
-            .where((c) => c.isVisible)
-            .map((c) => BarChartRodStackItemViewModel.fromStore(store, c))
-            .toList());
-  }
-
-  @override
-  List<Object?> get props => [control, rodStackItems];
-}
diff --git a/package/lib/src/models/barchart_view_model.dart b/package/lib/src/models/barchart_view_model.dart
deleted file mode 100644
index 5d1c64ba2..000000000
--- a/package/lib/src/models/barchart_view_model.dart
+++ /dev/null
@@ -1,61 +0,0 @@
-import 'package:equatable/equatable.dart';
-import 'package:redux/redux.dart';
-
-import 'app_state.dart';
-import 'barchart_group_view_model.dart';
-import 'chart_axis_view_model.dart';
-import 'control.dart';
-
-class BarChartViewModel extends Equatable {
-  final Control control;
-  final ChartAxisViewModel? leftAxis;
-  final ChartAxisViewModel? topAxis;
-  final ChartAxisViewModel? rightAxis;
-  final ChartAxisViewModel? bottomAxis;
-  final List<BarChartGroupViewModel> barGroups;
-  final dynamic dispatch;
-
-  const BarChartViewModel(
-      {required this.control,
-      required this.leftAxis,
-      required this.topAxis,
-      required this.rightAxis,
-      required this.bottomAxis,
-      required this.barGroups,
-      required this.dispatch});
-
-  static BarChartViewModel fromStore(
-      Store<AppState> store, Control control, List<Control> children) {
-    var leftAxisCtrls =
-        children.where((c) => c.type == "axis" && c.name == "l" && c.isVisible);
-    var topAxisCtrls =
-        children.where((c) => c.type == "axis" && c.name == "t" && c.isVisible);
-    var rightAxisCtrls =
-        children.where((c) => c.type == "axis" && c.name == "r" && c.isVisible);
-    var bottomAxisCtrls =
-        children.where((c) => c.type == "axis" && c.name == "b" && c.isVisible);
-    return BarChartViewModel(
-        control: control,
-        leftAxis: leftAxisCtrls.isNotEmpty
-            ? ChartAxisViewModel.fromStore(store, leftAxisCtrls.first)
-            : null,
-        topAxis: topAxisCtrls.isNotEmpty
-            ? ChartAxisViewModel.fromStore(store, topAxisCtrls.first)
-            : null,
-        rightAxis: rightAxisCtrls.isNotEmpty
-            ? ChartAxisViewModel.fromStore(store, rightAxisCtrls.first)
-            : null,
-        bottomAxis: bottomAxisCtrls.isNotEmpty
-            ? ChartAxisViewModel.fromStore(store, bottomAxisCtrls.first)
-            : null,
-        barGroups: children
-            .where((c) => c.type == "group" && c.isVisible)
-            .map((c) => BarChartGroupViewModel.fromStore(store, c))
-            .toList(),
-        dispatch: store.dispatch);
-  }
-
-  @override
-  List<Object?> get props =>
-      [control, leftAxis, rightAxis, topAxis, bottomAxis, barGroups, dispatch];
-}
diff --git a/package/lib/src/models/canvas_view_model.dart b/package/lib/src/models/canvas_view_model.dart
deleted file mode 100644
index cf7e9053c..000000000
--- a/package/lib/src/models/canvas_view_model.dart
+++ /dev/null
@@ -1,39 +0,0 @@
-import 'package:collection/collection.dart';
-import 'package:equatable/equatable.dart';
-import 'package:redux/redux.dart';
-
-import 'app_state.dart';
-import 'control.dart';
-import 'control_tree_view_model.dart';
-
-class CanvasViewModel extends Equatable {
-  final Control control;
-  final Control? child;
-  final List<ControlTreeViewModel> shapes;
-  final dynamic dispatch;
-
-  const CanvasViewModel(
-      {required this.control,
-      required this.child,
-      required this.shapes,
-      required this.dispatch});
-
-  static CanvasViewModel fromStore(
-      Store<AppState> store, Control control, List<Control> children) {
-    return CanvasViewModel(
-        control: control,
-        child: store.state.controls[control.id]!.childIds
-            .map((childId) => store.state.controls[childId])
-            .whereNotNull()
-            .where((c) => c.name == "content" && c.isVisible)
-            .firstOrNull,
-        shapes: children
-            .where((c) => c.name != "content" && c.isVisible)
-            .map((c) => ControlTreeViewModel.fromStore(store, c))
-            .toList(),
-        dispatch: store.dispatch);
-  }
-
-  @override
-  List<Object?> get props => [control, shapes, dispatch];
-}
diff --git a/package/lib/src/models/chart_axis_label_view_model.dart b/package/lib/src/models/chart_axis_label_view_model.dart
deleted file mode 100644
index 794fe0834..000000000
--- a/package/lib/src/models/chart_axis_label_view_model.dart
+++ /dev/null
@@ -1,27 +0,0 @@
-import 'package:collection/collection.dart';
-import 'package:equatable/equatable.dart';
-import 'package:redux/redux.dart';
-
-import 'app_state.dart';
-import 'control.dart';
-
-class ChartAxisLabelViewModel extends Equatable {
-  final double value;
-  final Control? control;
-
-  const ChartAxisLabelViewModel({required this.value, required this.control});
-
-  static ChartAxisLabelViewModel fromStore(
-      Store<AppState> store, Control control) {
-    return ChartAxisLabelViewModel(
-        value: control.attrDouble("value")!,
-        control: store.state.controls[control.id]!.childIds
-            .map((childId) => store.state.controls[childId])
-            .whereNotNull()
-            .where((c) => c.isVisible)
-            .firstOrNull);
-  }
-
-  @override
-  List<Object?> get props => [value, control];
-}
diff --git a/package/lib/src/models/control_children_view_model.dart b/package/lib/src/models/control_children_view_model.dart
deleted file mode 100644
index 6fb80b6d4..000000000
--- a/package/lib/src/models/control_children_view_model.dart
+++ /dev/null
@@ -1,24 +0,0 @@
-import 'package:equatable/equatable.dart';
-import 'package:redux/redux.dart';
-
-import 'app_state.dart';
-import 'control.dart';
-
-class ControlChildrenViewModel extends Equatable {
-  final List<Control> children;
-
-  const ControlChildrenViewModel({required this.children});
-
-  static ControlChildrenViewModel fromStore(Store<AppState> store, String id,
-      {dynamic dispatch}) {
-    return ControlChildrenViewModel(
-        children: store.state.controls[id] != null
-            ? store.state.controls[id]!.childIds
-                .map((childId) => store.state.controls[childId]!)
-                .toList()
-            : []);
-  }
-
-  @override
-  List<Object?> get props => [children];
-}
diff --git a/package/lib/src/models/linechart_data_point_view_model.dart b/package/lib/src/models/linechart_data_point_view_model.dart
deleted file mode 100644
index b39209bd3..000000000
--- a/package/lib/src/models/linechart_data_point_view_model.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-import 'package:equatable/equatable.dart';
-import 'package:redux/redux.dart';
-
-import 'app_state.dart';
-import 'control.dart';
-
-class LineChartDataPointViewModel extends Equatable {
-  final Control control;
-  final double x;
-  final double y;
-  final String? tooltip;
-
-  const LineChartDataPointViewModel(
-      {required this.control,
-      required this.x,
-      required this.y,
-      required this.tooltip});
-
-  static LineChartDataPointViewModel fromStore(
-      Store<AppState> store, Control control) {
-    return LineChartDataPointViewModel(
-        control: control,
-        x: control.attrDouble("x")!,
-        y: control.attrDouble("y")!,
-        tooltip: control.attrString("tooltip"));
-  }
-
-  @override
-  List<Object?> get props => [control];
-}
diff --git a/package/lib/src/models/linechart_data_view_model.dart b/package/lib/src/models/linechart_data_view_model.dart
deleted file mode 100644
index 07cd2bf11..000000000
--- a/package/lib/src/models/linechart_data_view_model.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-import 'package:collection/collection.dart';
-import 'package:equatable/equatable.dart';
-import 'package:redux/redux.dart';
-
-import 'app_state.dart';
-import 'control.dart';
-import 'linechart_data_point_view_model.dart';
-
-class LineChartDataViewModel extends Equatable {
-  final Control control;
-  final List<LineChartDataPointViewModel> dataPoints;
-
-  const LineChartDataViewModel(
-      {required this.control, required this.dataPoints});
-
-  static LineChartDataViewModel fromStore(
-      Store<AppState> store, Control control) {
-    return LineChartDataViewModel(
-        control: control,
-        dataPoints: store.state.controls[control.id]!.childIds
-            .map((childId) => store.state.controls[childId])
-            .whereNotNull()
-            .where((c) => c.isVisible)
-            .map((c) => LineChartDataPointViewModel.fromStore(store, c))
-            .toList());
-  }
-
-  @override
-  List<Object?> get props => [control, dataPoints];
-}
diff --git a/package/lib/src/models/linechart_event_data.dart b/package/lib/src/models/linechart_event_data.dart
deleted file mode 100644
index f158ddbcf..000000000
--- a/package/lib/src/models/linechart_event_data.dart
+++ /dev/null
@@ -1,32 +0,0 @@
-import 'package:equatable/equatable.dart';
-
-class LineChartEventData extends Equatable {
-  final String eventType;
-  final List<LineChartEventDataSpot> barSpots;
-
-  const LineChartEventData({required this.eventType, required this.barSpots});
-
-  Map<String, dynamic> toJson() => <String, dynamic>{
-        'type': eventType,
-        'spots': barSpots,
-      };
-
-  @override
-  List<Object?> get props => [eventType, barSpots];
-}
-
-class LineChartEventDataSpot extends Equatable {
-  final int barIndex;
-  final int spotIndex;
-
-  const LineChartEventDataSpot(
-      {required this.barIndex, required this.spotIndex});
-
-  Map<String, dynamic> toJson() => <String, dynamic>{
-        'bar_index': barIndex,
-        'spot_index': spotIndex,
-      };
-
-  @override
-  List<Object?> get props => [barIndex, spotIndex];
-}
diff --git a/package/lib/src/models/linechart_view_model.dart b/package/lib/src/models/linechart_view_model.dart
deleted file mode 100644
index c4d2d3546..000000000
--- a/package/lib/src/models/linechart_view_model.dart
+++ /dev/null
@@ -1,61 +0,0 @@
-import 'package:equatable/equatable.dart';
-import 'package:redux/redux.dart';
-
-import 'app_state.dart';
-import 'control.dart';
-import 'chart_axis_view_model.dart';
-import 'linechart_data_view_model.dart';
-
-class LineChartViewModel extends Equatable {
-  final Control control;
-  final ChartAxisViewModel? leftAxis;
-  final ChartAxisViewModel? topAxis;
-  final ChartAxisViewModel? rightAxis;
-  final ChartAxisViewModel? bottomAxis;
-  final List<LineChartDataViewModel> dataSeries;
-  final dynamic dispatch;
-
-  const LineChartViewModel(
-      {required this.control,
-      required this.leftAxis,
-      required this.topAxis,
-      required this.rightAxis,
-      required this.bottomAxis,
-      required this.dataSeries,
-      required this.dispatch});
-
-  static LineChartViewModel fromStore(
-      Store<AppState> store, Control control, List<Control> children) {
-    var leftAxisCtrls =
-        children.where((c) => c.type == "axis" && c.name == "l" && c.isVisible);
-    var topAxisCtrls =
-        children.where((c) => c.type == "axis" && c.name == "t" && c.isVisible);
-    var rightAxisCtrls =
-        children.where((c) => c.type == "axis" && c.name == "r" && c.isVisible);
-    var bottomAxisCtrls =
-        children.where((c) => c.type == "axis" && c.name == "b" && c.isVisible);
-    return LineChartViewModel(
-        control: control,
-        leftAxis: leftAxisCtrls.isNotEmpty
-            ? ChartAxisViewModel.fromStore(store, leftAxisCtrls.first)
-            : null,
-        topAxis: topAxisCtrls.isNotEmpty
-            ? ChartAxisViewModel.fromStore(store, topAxisCtrls.first)
-            : null,
-        rightAxis: rightAxisCtrls.isNotEmpty
-            ? ChartAxisViewModel.fromStore(store, rightAxisCtrls.first)
-            : null,
-        bottomAxis: bottomAxisCtrls.isNotEmpty
-            ? ChartAxisViewModel.fromStore(store, bottomAxisCtrls.first)
-            : null,
-        dataSeries: children
-            .where((c) => c.type == "data" && c.isVisible)
-            .map((c) => LineChartDataViewModel.fromStore(store, c))
-            .toList(),
-        dispatch: store.dispatch);
-  }
-
-  @override
-  List<Object?> get props =>
-      [control, leftAxis, rightAxis, topAxis, bottomAxis, dataSeries, dispatch];
-}
diff --git a/package/lib/src/models/page_view_model.dart b/package/lib/src/models/page_view_model.dart
deleted file mode 100644
index c654001a0..000000000
--- a/package/lib/src/models/page_view_model.dart
+++ /dev/null
@@ -1,24 +0,0 @@
-import 'package:equatable/equatable.dart';
-import 'package:redux/redux.dart';
-
-import 'app_state.dart';
-import 'control.dart';
-
-class PageViewModel extends Equatable {
-  final bool isLoading;
-  final String error;
-  final Control? page;
-
-  const PageViewModel(
-      {required this.isLoading, required this.error, this.page});
-
-  static PageViewModel fromStore(Store<AppState> store) {
-    return PageViewModel(
-        isLoading: store.state.isLoading,
-        error: store.state.error,
-        page: store.state.controls["page"]);
-  }
-
-  @override
-  List<Object?> get props => [isLoading, error, page];
-}
diff --git a/package/lib/src/models/piechart_event_data.dart b/package/lib/src/models/piechart_event_data.dart
deleted file mode 100644
index e2498ad92..000000000
--- a/package/lib/src/models/piechart_event_data.dart
+++ /dev/null
@@ -1,17 +0,0 @@
-import 'package:equatable/equatable.dart';
-
-class PieChartEventData extends Equatable {
-  final String eventType;
-  final int? sectionIndex;
-  // final double? angle;
-  // final double? radius;
-
-  const PieChartEventData(
-      {required this.eventType, required this.sectionIndex});
-
-  Map<String, dynamic> toJson() =>
-      <String, dynamic>{'type': eventType, 'section_index': sectionIndex};
-
-  @override
-  List<Object?> get props => [eventType, sectionIndex];
-}
diff --git a/package/lib/src/models/piechart_section_view_model.dart b/package/lib/src/models/piechart_section_view_model.dart
deleted file mode 100644
index d6b40f041..000000000
--- a/package/lib/src/models/piechart_section_view_model.dart
+++ /dev/null
@@ -1,28 +0,0 @@
-import 'package:collection/collection.dart';
-import 'package:equatable/equatable.dart';
-import 'package:redux/redux.dart';
-
-import 'app_state.dart';
-import 'control.dart';
-
-class PieChartSectionViewModel extends Equatable {
-  final Control control;
-  final Control? badge;
-
-  const PieChartSectionViewModel({required this.control, required this.badge});
-
-  static PieChartSectionViewModel fromStore(
-      Store<AppState> store, Control control) {
-    var children = store.state.controls[control.id]!.childIds
-        .map((childId) => store.state.controls[childId])
-        .whereNotNull()
-        .where((c) => c.isVisible);
-
-    return PieChartSectionViewModel(
-        control: control,
-        badge: children.firstWhereOrNull((c) => c.name == "badge"));
-  }
-
-  @override
-  List<Object?> get props => [control, badge];
-}
diff --git a/package/lib/src/models/piechart_view_model.dart b/package/lib/src/models/piechart_view_model.dart
deleted file mode 100644
index 960a6fb5d..000000000
--- a/package/lib/src/models/piechart_view_model.dart
+++ /dev/null
@@ -1,29 +0,0 @@
-import 'package:equatable/equatable.dart';
-import 'package:redux/redux.dart';
-
-import '../models/piechart_section_view_model.dart';
-import 'app_state.dart';
-import 'control.dart';
-
-class PieChartViewModel extends Equatable {
-  final Control control;
-  final List<PieChartSectionViewModel> sections;
-  final dynamic dispatch;
-
-  const PieChartViewModel(
-      {required this.control, required this.sections, required this.dispatch});
-
-  static PieChartViewModel fromStore(
-      Store<AppState> store, Control control, List<Control> children) {
-    return PieChartViewModel(
-        control: control,
-        sections: children
-            .where((c) => c.type == "section" && c.isVisible)
-            .map((c) => PieChartSectionViewModel.fromStore(store, c))
-            .toList(),
-        dispatch: store.dispatch);
-  }
-
-  @override
-  List<Object?> get props => [control, sections, dispatch];
-}
diff --git a/package/lib/src/models/routes_view_model.dart b/package/lib/src/models/routes_view_model.dart
deleted file mode 100644
index d72e1045e..000000000
--- a/package/lib/src/models/routes_view_model.dart
+++ /dev/null
@@ -1,45 +0,0 @@
-import 'package:collection/collection.dart';
-import 'package:equatable/equatable.dart';
-import 'package:redux/redux.dart';
-
-import 'app_state.dart';
-import 'control.dart';
-
-class RoutesViewModel extends Equatable {
-  final Control page;
-  final bool isLoading;
-  final String error;
-  final List<Control> offstageControls;
-  final List<Control> views;
-
-  const RoutesViewModel(
-      {required this.page,
-      required this.isLoading,
-      required this.error,
-      required this.offstageControls,
-      required this.views});
-
-  static RoutesViewModel fromStore(Store<AppState> store) {
-    Control? offstageControl = store.state.controls["page"]!.childIds
-        .map((childId) => store.state.controls[childId]!)
-        .firstWhereOrNull((c) => c.type == "offstage");
-
-    return RoutesViewModel(
-        page: store.state.controls["page"]!,
-        isLoading: store.state.isLoading,
-        error: store.state.error,
-        offstageControls: offstageControl != null
-            ? store.state.controls[offstageControl.id]!.childIds
-                .map((childId) => store.state.controls[childId]!)
-                .where((c) => c.isVisible)
-                .toList()
-            : [],
-        views: store.state.controls["page"]!.childIds
-            .map((childId) => store.state.controls[childId]!)
-            .where((c) => c.type != "offstage" && c.isVisible)
-            .toList());
-  }
-
-  @override
-  List<Object?> get props => [page, isLoading, error, offstageControls, views];
-}
diff --git a/package/lib/src/protocol/container_tap_event.dart b/package/lib/src/protocol/container_tap_event.dart
deleted file mode 100644
index 3ca0d82b7..000000000
--- a/package/lib/src/protocol/container_tap_event.dart
+++ /dev/null
@@ -1,19 +0,0 @@
-class ContainerTapEvent {
-  final double localX;
-  final double localY;
-  final double globalX;
-  final double globalY;
-
-  ContainerTapEvent(
-      {required this.localX,
-      required this.localY,
-      required this.globalX,
-      required this.globalY});
-
-  Map<String, dynamic> toJson() => <String, dynamic>{
-        'lx': localX,
-        'ly': localY,
-        'gx': globalX,
-        'gy': globalY
-      };
-}
diff --git a/package/lib/src/protocol/drag_target_accept_event.dart b/package/lib/src/protocol/drag_target_accept_event.dart
deleted file mode 100644
index 0153520d9..000000000
--- a/package/lib/src/protocol/drag_target_accept_event.dart
+++ /dev/null
@@ -1,17 +0,0 @@
-class DragTargetAcceptEvent {
-  final String srcId;
-  final double x;
-  final double y;
-
-  DragTargetAcceptEvent({
-    required this.srcId,
-    required this.x,
-    required this.y,
-  });
-
-  Map<String, dynamic> toJson() => <String, dynamic>{
-        'src_id': srcId,
-        'x': x,
-        'y': y,
-      };
-}
diff --git a/package/lib/src/protocol/file_picker_result_event.dart b/package/lib/src/protocol/file_picker_result_event.dart
deleted file mode 100644
index a078dcb80..000000000
--- a/package/lib/src/protocol/file_picker_result_event.dart
+++ /dev/null
@@ -1,22 +0,0 @@
-class FilePickerResultEvent {
-  final String? path;
-  final List<FilePickerFile>? files;
-
-  FilePickerResultEvent({required this.path, required this.files});
-
-  Map<String, dynamic> toJson() => <String, dynamic>{
-        'path': path,
-        'files': files?.map((f) => f.toJson()).toList()
-      };
-}
-
-class FilePickerFile {
-  final String name;
-  final String? path;
-  final int size;
-
-  FilePickerFile({required this.name, required this.path, required this.size});
-
-  Map<String, dynamic> toJson() =>
-      <String, dynamic>{'name': name, 'path': path, 'size': size};
-}
diff --git a/package/lib/src/protocol/file_picker_upload_file.dart b/package/lib/src/protocol/file_picker_upload_file.dart
deleted file mode 100644
index f64fa23b4..000000000
--- a/package/lib/src/protocol/file_picker_upload_file.dart
+++ /dev/null
@@ -1,8 +0,0 @@
-class FilePickerUploadFile {
-  final String name;
-  final String uploadUrl;
-  final String method;
-
-  FilePickerUploadFile(
-      {required this.name, required this.uploadUrl, required this.method});
-}
diff --git a/package/lib/src/protocol/file_picker_upload_progress_event.dart b/package/lib/src/protocol/file_picker_upload_progress_event.dart
deleted file mode 100644
index 7c02a4c4e..000000000
--- a/package/lib/src/protocol/file_picker_upload_progress_event.dart
+++ /dev/null
@@ -1,14 +0,0 @@
-class FilePickerUploadProgressEvent {
-  final String name;
-  final double? progress;
-  final String? error;
-
-  FilePickerUploadProgressEvent(
-      {required this.name, required this.progress, required this.error});
-
-  Map<String, dynamic> toJson() => <String, dynamic>{
-        'file_name': name,
-        'progress': progress,
-        'error': error
-      };
-}
diff --git a/package/lib/src/protocol/keyboard_event.dart b/package/lib/src/protocol/keyboard_event.dart
deleted file mode 100644
index 7c25e614f..000000000
--- a/package/lib/src/protocol/keyboard_event.dart
+++ /dev/null
@@ -1,22 +0,0 @@
-class KeyboardEvent {
-  final String key;
-  final bool isShiftPressed;
-  final bool isControlPressed;
-  final bool isAltPressed;
-  final bool isMetaPressed;
-
-  KeyboardEvent(
-      {required this.key,
-      required this.isShiftPressed,
-      required this.isControlPressed,
-      required this.isAltPressed,
-      required this.isMetaPressed});
-
-  Map<String, dynamic> toJson() => <String, dynamic>{
-        'key': key,
-        'shift': isShiftPressed,
-        'ctrl': isControlPressed,
-        'alt': isAltPressed,
-        'meta': isMetaPressed
-      };
-}
diff --git a/package/lib/src/reducers.dart b/package/lib/src/reducers.dart
index a833ab154..450363b2f 100644
--- a/package/lib/src/reducers.dart
+++ b/package/lib/src/reducers.dart
@@ -1,10 +1,10 @@
 import 'dart:convert';
 
-import 'package:flet/src/flet_server.dart';
 import 'package:flutter/foundation.dart';
 import 'package:url_launcher/url_launcher.dart';
 
 import 'actions.dart';
+import 'flet_server.dart';
 import 'models/app_state.dart';
 import 'models/control.dart';
 import 'models/window_media_data.dart';
diff --git a/package/lib/src/utils/buttons.dart b/package/lib/src/utils/buttons.dart
index c54a70e4d..57845f8bc 100644
--- a/package/lib/src/utils/buttons.dart
+++ b/package/lib/src/utils/buttons.dart
@@ -39,18 +39,6 @@ ButtonStyle? parseButtonStyle(ThemeData theme, Control control, String propName,
       defaultShape);
 }
 
-MaterialStateProperty<Color?>? parseMaterialStateColor(
-    ThemeData theme, Control control, String propName) {
-  var v = control.attrString(propName, null);
-  if (v == null) {
-    return null;
-  }
-
-  final j1 = json.decode(v);
-  return getMaterialStateProperty<Color?>(
-      j1, (jv) => HexColor.fromString(theme, jv as String), null);
-}
-
 ButtonStyle? buttonStyleFromJSON(
     ThemeData theme,
     Map<String, dynamic> json,
diff --git a/package/lib/src/utils/colors.dart b/package/lib/src/utils/colors.dart
index bd98b6134..158319b93 100644
--- a/package/lib/src/utils/colors.dart
+++ b/package/lib/src/utils/colors.dart
@@ -1,6 +1,10 @@
+import 'dart:convert';
+
 import 'package:flutter/material.dart';
 
+import '../models/control.dart';
 import 'cupertino_colors.dart';
+import 'material_state.dart';
 import 'numbers.dart';
 
 Color? _getThemeColor(ThemeData theme, String colorName) {
@@ -231,3 +235,15 @@ extension ColorExtension on Color {
     );
   }
 }
+
+MaterialStateProperty<Color?>? parseMaterialStateColor(
+    ThemeData theme, Control control, String propName) {
+  var v = control.attrString(propName, null);
+  if (v == null) {
+    return null;
+  }
+
+  final j1 = json.decode(v);
+  return getMaterialStateProperty<Color?>(
+      j1, (jv) => HexColor.fromString(theme, jv as String), null);
+}
diff --git a/package/lib/src/utils/dismissible.dart b/package/lib/src/utils/dismissible.dart
index 9e504ef97..94d0168ec 100644
--- a/package/lib/src/utils/dismissible.dart
+++ b/package/lib/src/utils/dismissible.dart
@@ -1,9 +1,9 @@
 import 'dart:convert';
 
-import 'package:flet/src/utils/numbers.dart';
 import 'package:flutter/material.dart';
 
 import '../models/control.dart';
+import '../utils/numbers.dart';
 
 Map<DismissDirection, double>? parseDismissThresholds(
     Control control, String propName) {
diff --git a/package/lib/src/utils/images.dart b/package/lib/src/utils/images.dart
index f2b274d45..f545dad0d 100644
--- a/package/lib/src/utils/images.dart
+++ b/package/lib/src/utils/images.dart
@@ -2,11 +2,11 @@ import 'dart:convert';
 import 'dart:ui';
 
 import 'package:collection/collection.dart';
-import 'package:flet/src/utils/numbers.dart';
 import 'package:flutter/material.dart';
 
 import '../models/control.dart';
 import 'gradient.dart';
+import 'numbers.dart';
 
 export 'images_io.dart' if (dart.library.js) "images_web.dart";
 
diff --git a/package/lib/src/utils/menu.dart b/package/lib/src/utils/menu.dart
index 8d9478bf3..0bd4a29f1 100644
--- a/package/lib/src/utils/menu.dart
+++ b/package/lib/src/utils/menu.dart
@@ -41,18 +41,6 @@ MenuStyle? parseMenuStyle(ThemeData theme, Control control, String propName,
       defaultShape);
 }
 
-MaterialStateProperty<Color?>? parseMaterialStateColor(
-    ThemeData theme, Control control, String propName) {
-  var v = control.attrString(propName, null);
-  if (v == null) {
-    return null;
-  }
-
-  final j1 = json.decode(v);
-  return getMaterialStateProperty<Color?>(
-      j1, (jv) => HexColor.fromString(theme, jv as String), null);
-}
-
 MenuStyle? menuStyleFromJSON(
     ThemeData theme,
     Map<String, dynamic> json,
diff --git a/package/lib/src/utils/text.dart b/package/lib/src/utils/text.dart
index 9b8ac878a..22b2b14cb 100644
--- a/package/lib/src/utils/text.dart
+++ b/package/lib/src/utils/text.dart
@@ -5,7 +5,6 @@ import 'package:collection/collection.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 
-import '../flet_server.dart';
 import '../models/control.dart';
 import '../models/control_tree_view_model.dart';
 import '../utils/drawing.dart';
@@ -79,16 +78,22 @@ FontWeight? getFontWeight(String weightName) {
   return null;
 }
 
-List<InlineSpan> parseTextSpans(ThemeData theme, ControlTreeViewModel viewModel,
-    bool parentDisabled, FletServer? server) {
+List<InlineSpan> parseTextSpans(
+    ThemeData theme,
+    ControlTreeViewModel viewModel,
+    bool parentDisabled,
+    void Function(String, String, String)? sendControlEvent) {
   return viewModel.children
-      .map((c) => parseInlineSpan(theme, c, parentDisabled, server))
+      .map((c) => parseInlineSpan(theme, c, parentDisabled, sendControlEvent))
       .whereNotNull()
       .toList();
 }
 
-InlineSpan? parseInlineSpan(ThemeData theme, ControlTreeViewModel spanViewModel,
-    bool parentDisabled, FletServer? server) {
+InlineSpan? parseInlineSpan(
+    ThemeData theme,
+    ControlTreeViewModel spanViewModel,
+    bool parentDisabled,
+    void Function(String, String, String)? sendControlEvent) {
   if (spanViewModel.control.type == "textspan") {
     bool disabled = spanViewModel.control.isDisabled || parentDisabled;
     var onClick = spanViewModel.control.attrBool("onClick", false)!;
@@ -97,45 +102,38 @@ InlineSpan? parseInlineSpan(ThemeData theme, ControlTreeViewModel spanViewModel,
     return TextSpan(
       text: spanViewModel.control.attrString("text"),
       style: parseTextStyle(theme, spanViewModel.control, "style"),
-      children: parseTextSpans(theme, spanViewModel, parentDisabled, server),
-      mouseCursor: onClick && !disabled && server != null
+      children: parseTextSpans(
+          theme, spanViewModel, parentDisabled, sendControlEvent),
+      mouseCursor: onClick && !disabled && sendControlEvent != null
           ? SystemMouseCursors.click
           : null,
-      recognizer: (onClick || url != "") && !disabled && server != null
-          ? (TapGestureRecognizer()
-            ..onTap = () {
-              debugPrint("TextSpan ${spanViewModel.control.id} clicked!");
-              if (url != "") {
-                openWebBrowser(url, webWindowName: urlTarget);
-              }
-              if (onClick) {
-                server.sendPageEvent(
-                    eventTarget: spanViewModel.control.id,
-                    eventName: "click",
-                    eventData: "");
-              }
-            })
-          : null,
+      recognizer:
+          (onClick || url != "") && !disabled && sendControlEvent != null
+              ? (TapGestureRecognizer()
+                ..onTap = () {
+                  debugPrint("TextSpan ${spanViewModel.control.id} clicked!");
+                  if (url != "") {
+                    openWebBrowser(url, webWindowName: urlTarget);
+                  }
+                  if (onClick) {
+                    sendControlEvent(spanViewModel.control.id, "click", "");
+                  }
+                })
+              : null,
       onEnter: spanViewModel.control.attrBool("onEnter", false)! &&
               !disabled &&
-              server != null
+              sendControlEvent != null
           ? (event) {
               debugPrint("TextSpan ${spanViewModel.control.id} entered!");
-              server.sendPageEvent(
-                  eventTarget: spanViewModel.control.id,
-                  eventName: "enter",
-                  eventData: "");
+              sendControlEvent(spanViewModel.control.id, "enter", "");
             }
           : null,
       onExit: spanViewModel.control.attrBool("onExit", false)! &&
               !disabled &&
-              server != null
+              sendControlEvent != null
           ? (event) {
               debugPrint("TextSpan ${spanViewModel.control.id} exited!");
-              server.sendPageEvent(
-                  eventTarget: spanViewModel.control.id,
-                  eventName: "exit",
-                  eventData: "");
+              sendControlEvent(spanViewModel.control.id, "exit", "");
             }
           : null,
     );
diff --git a/package/lib/src/utils/textfield.dart b/package/lib/src/utils/textfield.dart
index fb35d25e2..9e7e003d2 100644
--- a/package/lib/src/utils/textfield.dart
+++ b/package/lib/src/utils/textfield.dart
@@ -1,10 +1,10 @@
 import 'dart:convert';
 
-import 'package:flet/src/utils/numbers.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 
 import '../models/control.dart';
+import '../utils/numbers.dart';
 
 FilteringTextInputFormatter? parseInputFilter(
     Control control, String propName) {
diff --git a/package/lib/src/utils/theme.dart b/package/lib/src/utils/theme.dart
index 927600682..689cf1b7e 100644
--- a/package/lib/src/utils/theme.dart
+++ b/package/lib/src/utils/theme.dart
@@ -1,14 +1,14 @@
 import 'dart:convert';
 
-import 'package:flet/src/utils/borders.dart';
-import 'package:flet/src/utils/edge_insets.dart';
-import 'package:flet/src/utils/numbers.dart';
-import 'package:flet/src/utils/text.dart';
 import 'package:flutter/material.dart';
 
 import '../models/control.dart';
+import 'borders.dart';
 import 'colors.dart';
+import 'edge_insets.dart';
 import 'material_state.dart';
+import 'numbers.dart';
+import 'text.dart';
 
 ThemeData parseTheme(Control control, String propName, Brightness? brightness,
     {ThemeData? parentTheme}) {
@@ -47,7 +47,6 @@ ThemeData themeFromJson(Map<String, dynamic>? json, Brightness? brightness,
       useMaterial3: json?["use_material3"] ?? primarySwatch == null);
 
   return theme.copyWith(
-      useMaterial3: json?["use_material3"] ?? theme.useMaterial3,
       visualDensity: json?["visual_density"] != null
           ? parseVisualDensity(json?["visual_density"])
           : theme.visualDensity,
diff --git a/package/lib/src/widgets/window_media.dart b/package/lib/src/widgets/window_media.dart
index e00cd077b..750b7ceec 100644
--- a/package/lib/src/widgets/window_media.dart
+++ b/package/lib/src/widgets/window_media.dart
@@ -1,14 +1,13 @@
 import 'package:flutter/widgets.dart';
-import 'package:flutter_redux/flutter_redux.dart';
 import 'package:window_manager/window_manager.dart';
 
 import '../actions.dart';
 import '../flet_app_services.dart';
-import '../models/app_state.dart';
 import '../utils/desktop.dart';
 
 class WindowMedia extends StatefulWidget {
-  const WindowMedia({super.key});
+  final dynamic dispatch;
+  const WindowMedia({super.key, required this.dispatch});
 
   @override
   // ignore: library_private_types_in_public_api
@@ -16,8 +15,6 @@ class WindowMedia extends StatefulWidget {
 }
 
 class WindowMediaState extends State<WindowMedia> with WindowListener {
-  Function? _dispatch;
-
   @override
   void initState() {
     super.initState();
@@ -32,13 +29,7 @@ class WindowMediaState extends State<WindowMedia> with WindowListener {
 
   @override
   Widget build(BuildContext context) {
-    return StoreConnector<AppState, Function>(
-        distinct: true,
-        converter: (store) => store.dispatch,
-        builder: (context, dispatch) {
-          _dispatch = dispatch;
-          return const SizedBox.shrink();
-        });
+    return const SizedBox.shrink();
   }
 
   @override
@@ -52,7 +43,7 @@ class WindowMediaState extends State<WindowMedia> with WindowListener {
     debugPrint('[WindowManager] onWindowEvent: $eventName');
     getWindowMediaData().then((wmd) {
       debugPrint("WindowMediaData: $wmd");
-      _dispatch!(WindowEventAction(
+      widget.dispatch!(WindowEventAction(
           eventName, wmd, FletAppServices.of(context).server));
     });
   }
diff --git a/package/test/models/linechart_event_data_test.dart b/package/test/models/linechart_event_data_test.dart
index 83f70fbc9..9cff525e2 100644
--- a/package/test/models/linechart_event_data_test.dart
+++ b/package/test/models/linechart_event_data_test.dart
@@ -1,6 +1,6 @@
 import 'dart:convert';
 
-import 'package:flet/src/models/linechart_event_data.dart';
+import 'package:flet/src/controls/linechart.dart';
 import 'package:flutter_test/flutter_test.dart';
 
 void main() {
diff --git a/sdk/python/packages/flet-core/src/flet_core/__init__.py b/sdk/python/packages/flet-core/src/flet_core/__init__.py
index c5b158cb1..d4132fdd8 100644
--- a/sdk/python/packages/flet-core/src/flet_core/__init__.py
+++ b/sdk/python/packages/flet-core/src/flet_core/__init__.py
@@ -234,7 +234,7 @@
     MaterialState,
     NotchShape,
     PaddingValue,
-    PageDesignLanguage,
+    PagePlatform,
     ScrollMode,
     TabAlignment,
     TextAlign,
diff --git a/sdk/python/packages/flet-core/src/flet_core/page.py b/sdk/python/packages/flet-core/src/flet_core/page.py
index c856f6bbe..539127a4b 100644
--- a/sdk/python/packages/flet-core/src/flet_core/page.py
+++ b/sdk/python/packages/flet-core/src/flet_core/page.py
@@ -41,8 +41,7 @@
     MainAxisAlignment,
     OffsetValue,
     PaddingValue,
-    PageDesignLanguage,
-    PageDesignString,
+    PagePlatform,
     ScrollMode,
     ThemeMode,
     ThemeModeString,
@@ -1227,7 +1226,13 @@ def debug(self) -> bool:
     # platform
     @property
     def platform(self):
-        return self._get_attr("platform")
+        return PagePlatform(self._get_attr("platform"))
+
+    @platform.setter
+    def platform(self, value: PagePlatform):
+        self._set_attr(
+            "platform", value.value if isinstance(value, PagePlatform) else value
+        )
 
     # platform_brightness
     @property
@@ -1246,22 +1251,6 @@ def client_ip(self):
     def client_user_agent(self):
         return self._get_attr("clientUserAgent")
 
-    # design
-    @property
-    def design(self) -> Optional[PageDesignLanguage]:
-        return self.__design
-
-    @design.setter
-    def design(self, value: Optional[PageDesignLanguage]):
-        self.__design = value
-        if isinstance(value, PageDesignLanguage):
-            self._set_attr("design", value.value)
-        else:
-            self.__set_design(value)
-
-    def __set_design(self, value: PageDesignString):
-        self._set_attr("design", value)
-
     # fonts
     @property
     def fonts(self) -> Optional[Dict[str, str]]:
diff --git a/sdk/python/packages/flet-core/src/flet_core/types.py b/sdk/python/packages/flet-core/src/flet_core/types.py
index c07f9b4e9..451a70962 100644
--- a/sdk/python/packages/flet-core/src/flet_core/types.py
+++ b/sdk/python/packages/flet-core/src/flet_core/types.py
@@ -286,15 +286,12 @@ class ImageRepeat(Enum):
     REPEAT_Y = "repeatY"
 
 
-PageDesignString = Literal[None, "material", "cupertino", "fluent", "macos", "adaptive"]
-
-
-class PageDesignLanguage(Enum):
-    MATERIAL = "material"
-    CUPERTINO = "cupertino"
-    FLUENT = "fluent"
+class PagePlatform(Enum):
+    IOS = "ios"
+    ANDROID = "android"
     MACOS = "macos"
-    ADAPTIVE = "adaptive"
+    WINDOWS = "windows"
+    LINUX = "linux"
 
 
 ThemeModeString = Literal[None, "system", "light", "dark"]
diff --git a/sdk/python/packages/flet/src/flet/cli/commands/build.py b/sdk/python/packages/flet/src/flet/cli/commands/build.py
index 06b059fe8..b8adc8225 100644
--- a/sdk/python/packages/flet/src/flet/cli/commands/build.py
+++ b/sdk/python/packages/flet/src/flet/cli/commands/build.py
@@ -9,7 +9,9 @@
 import tempfile
 import urllib.request
 from pathlib import Path
+from typing import Optional
 
+import flet.version
 import yaml
 from flet.cli.commands.base import BaseCommand
 from flet_core.utils import random_string, slugify
@@ -20,6 +22,7 @@
     from ctypes import windll
 
 PYODIDE_ROOT_URL = "https://cdn.jsdelivr.net/pyodide/v0.24.1/full"
+DEFAULT_TEMPLATE_URL = "gh:flet-dev/flet-build-template"
 
 
 class Command(BaseCommand):
@@ -235,7 +238,6 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
             "--template",
             dest="template",
             type=str,
-            default="gh:flet-dev/flet-build-template",
             help="a directory containing Flutter bootstrap template, or a URL to a git repository template",
         )
         parser.add_argument(
@@ -254,10 +256,17 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
     def handle(self, options: argparse.Namespace) -> None:
         from cookiecutter.main import cookiecutter
 
+        self.verbose = options.verbose
+        self.flutter_dir = None
+
         # get `flutter` and `dart` executables from PATH
         flutter_exe = self.find_flutter_batch("flutter")
         dart_exe = self.find_flutter_batch("dart")
 
+        if self.verbose > 1:
+            print("Flutter executable:", flutter_exe)
+            print("Dart executable:", dart_exe)
+
         target_platform = options.target_platform.lower()
         # platform check
         current_platform = platform.system()
@@ -266,25 +275,22 @@ def handle(self, options: argparse.Namespace) -> None:
             if current_platform == "Darwin":
                 current_platform = "macOS"
 
-            print(f"Can't build {target_platform} on {current_platform}")
-            sys.exit(1)
-
-        self.verbose = options.verbose
+            self.cleanup(1, f"Can't build {target_platform} on {current_platform}")
 
         python_app_path = Path(options.python_app_path).resolve()
         if not os.path.exists(python_app_path) or not os.path.isdir(python_app_path):
-            print(
-                f"Path to Flet app does not exist or is not a directory: {python_app_path}"
+            self.cleanup(
+                1,
+                f"Path to Flet app does not exist or is not a directory: {python_app_path}",
             )
-            sys.exit(1)
 
         python_module_name = Path(options.module_name).stem
         python_module_filename = f"{python_module_name}.py"
         if not os.path.exists(os.path.join(python_app_path, python_module_filename)):
-            print(
-                f"{python_module_filename} not found in the root of Flet app directory. Use --module-name option to specify an entry point for your Flet app."
+            self.cleanup(
+                1,
+                f"{python_module_filename} not found in the root of Flet app directory. Use --module-name option to specify an entry point for your Flet app.",
             )
-            sys.exit(1)
 
         self.flutter_dir = Path(tempfile.gettempdir()).joinpath(
             f"flet_flutter_build_{random_string(10)}"
@@ -311,6 +317,7 @@ def handle(self, options: argparse.Namespace) -> None:
         project_name = slugify(
             options.project_name if options.project_name else python_app_path.name
         ).replace("-", "_")
+
         template_data["project_name"] = project_name
 
         if options.description is not None:
@@ -337,24 +344,65 @@ def handle(self, options: argparse.Namespace) -> None:
             "true" if options.use_color_emoji else "false"
         )
 
+        src_pubspec = None
+        src_pubspec_path = python_app_path.joinpath("pubspec.yaml")
+        if src_pubspec_path.exists():
+            with open(src_pubspec_path, encoding="utf8") as f:
+                src_pubspec = pubspec = yaml.safe_load(f)
+
+        flutter_dependencies = []
+        if src_pubspec and src_pubspec["dependencies"]:
+            for dep in src_pubspec["dependencies"].keys():
+                flutter_dependencies.append(dep)
+
+        template_data["flutter"] = {"dependencies": flutter_dependencies}
+
+        template_url = options.template
+        template_ref = options.template_ref
+        if not template_url:
+            template_url = DEFAULT_TEMPLATE_URL
+            if flet.version.version and not template_ref:
+                template_ref = flet.version.version
+
         # create Flutter project from a template
         print("Creating Flutter bootstrap project...", end="")
-        cookiecutter(
-            template=options.template,
-            checkout=options.template_ref,
-            directory=options.template_dir,
-            output_dir=str(self.flutter_dir.parent),
-            no_input=True,
-            overwrite_if_exists=True,
-            extra_context=template_data,
-        )
+        try:
+            cookiecutter(
+                template=template_url,
+                checkout=template_ref,
+                directory=options.template_dir,
+                output_dir=str(self.flutter_dir.parent),
+                no_input=True,
+                overwrite_if_exists=True,
+                extra_context=template_data,
+            )
+        except Exception as e:
+            self.cleanup(1, f"{e}")
         print("[spring_green3]OK[/spring_green3]")
 
         # load pubspec.yaml
         pubspec_path = str(self.flutter_dir.joinpath("pubspec.yaml"))
-        with open(pubspec_path) as f:
+        with open(pubspec_path, encoding="utf8") as f:
             pubspec = yaml.safe_load(f)
 
+        # merge dependencies to a dest pubspec.yaml
+        if src_pubspec and src_pubspec["dependencies"]:
+            for k, v in src_pubspec["dependencies"].items():
+                pubspec["dependencies"][k] = "any"
+
+        if src_pubspec and src_pubspec["dependency_overrides"]:
+            pubspec["dependency_overrides"] = {}
+            for k, v in src_pubspec["dependency_overrides"].items():
+                pubspec["dependency_overrides"][k] = v
+
+        # make sure project name is not named as any of dependencies
+        for dep in pubspec["dependencies"].keys():
+            if dep == project_name:
+                self.cleanup(
+                    1,
+                    f"Project name cannot have the same name as one of its dependencies: {dep}. Use --project option to specify a different project name.",
+                )
+
         # copy icons to `flutter_dir`
         print("Customizing app icons and splash images...", end="")
         assets_path = python_app_path.joinpath("assets")
@@ -510,7 +558,7 @@ def fallback_image(yaml_path: str, images: list):
         print("[spring_green3]OK[/spring_green3]")
 
         # save pubspec.yaml
-        with open(pubspec_path, "w") as f:
+        with open(pubspec_path, "w", encoding="utf8") as f:
             yaml.dump(pubspec, f)
 
         # generate icons
@@ -604,12 +652,11 @@ def fallback_image(yaml_path: str, images: list):
         # make sure app/app.zip exists
         app_zip_path = self.flutter_dir.joinpath("app", "app.zip")
         if not os.path.exists(app_zip_path):
-            print("Flet app package app/app.zip was not created.")
-            self.cleanup(1)
+            self.cleanup(1, "Flet app package app/app.zip was not created.")
 
         # create {flutter_dir}/app/app.hash
         app_hash_path = self.flutter_dir.joinpath("app", "app.zip.hash")
-        with open(app_hash_path, "w") as hf:
+        with open(app_hash_path, "w", encoding="utf8") as hf:
             hf.write(calculate_file_hash(app_zip_path))
         print("[spring_green3]OK[/spring_green3]")
 
@@ -638,8 +685,8 @@ def fallback_image(yaml_path: str, images: list):
                 for flutter_build_arg in flutter_build_arg_arr:
                     build_args.append(flutter_build_arg)
 
-        if self.verbose > 0:
-            print(build_args)
+        if self.verbose > 1:
+            build_args.append("--verbose")
 
         build_result = self.run(build_args, cwd=str(self.flutter_dir))
 
@@ -686,16 +733,13 @@ def ignore_build_output(path, files):
 
         print("[spring_green3]OK[/spring_green3]")
 
-        # print(self.flutter_dir)
-        # return
-
         self.cleanup(0)
 
     def create_pyodide_find_links(self):
         with urllib.request.urlopen(f"{PYODIDE_ROOT_URL}/pyodide-lock.json") as j:
             data = json.load(j)
         find_links_path = str(self.flutter_dir.joinpath("find-links.html"))
-        with open(find_links_path, "w") as f:
+        with open(find_links_path, "w", encoding="utf8") as f:
             for package in data["packages"].values():
                 file_name = package["file_name"]
                 f.write(f'<a href="{PYODIDE_ROOT_URL}/{file_name}">{file_name}</a>\n')
@@ -713,10 +757,10 @@ def copy_icon_image(self, src_path: Path, dest_path: Path, image_name: str):
     def find_flutter_batch(self, exe_filename: str):
         batch_path = shutil.which(exe_filename)
         if not batch_path:
-            print(
-                f"`{exe_filename}` command is not available in PATH. Install Flutter SDK."
+            self.cleanup(
+                1,
+                f"`{exe_filename}` command is not available in PATH. Install Flutter SDK.",
             )
-            sys.exit(1)
         if is_windows() and batch_path.endswith(".file"):
             return batch_path.replace(".file", ".bat")
         return batch_path
@@ -728,10 +772,13 @@ def run(self, args, cwd):
             previousCp = windll.kernel32.GetConsoleOutputCP()
             windll.kernel32.SetConsoleOutputCP(65001)
 
+        if self.verbose > 0:
+            print(f"\nRun subprocess: {args}")
+
         r = subprocess.run(
             args,
             cwd=cwd,
-            capture_output=self.verbose < 2,
+            capture_output=self.verbose < 1,
             text=True,
             encoding="utf8",
         )
@@ -742,14 +789,19 @@ def run(self, args, cwd):
 
         return r
 
-    def cleanup(self, exit_code: int):
-        if self.verbose > 0:
-            print(f"Deleting Flutter bootstrap directory {self.flutter_dir}")
-        shutil.rmtree(str(self.flutter_dir), ignore_errors=False, onerror=None)
+    def cleanup(self, exit_code: int, message: Optional[str] = None):
+        if self.flutter_dir and os.path.exists(self.flutter_dir):
+            if self.verbose > 0:
+                print(f"Deleting Flutter bootstrap directory {self.flutter_dir}")
+            shutil.rmtree(str(self.flutter_dir), ignore_errors=True, onerror=None)
         if exit_code == 0:
-            print("[spring_green3]Success![/spring_green3]")
+            msg = message if message else "Success!"
+            print(f"[spring_green3]{msg}[/spring_green3]")
         else:
-            print(
-                "[red]Error building Flet app - see the log of failed command above.[/red]"
+            msg = (
+                message
+                if message
+                else "Error building Flet app - see the log of failed command above."
             )
+            print(f"[red]{msg}[/red]")
         sys.exit(exit_code)