<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://roszkowski.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://roszkowski.dev/" rel="alternate" type="text/html" /><updated>2026-06-05T23:04:18+02:00</updated><id>https://roszkowski.dev/feed.xml</id><title type="html">Dominik Roszkowski Online Space</title><subtitle>Blogging about mobile development with Flutter</subtitle><author><name>Dominik Roszkowski</name><email>dominik@roszkowski.dev</email></author><entry><title type="html">Building TUIs with Dart has never been easier</title><link href="https://roszkowski.dev/2026/dart-tui-nocterm/" rel="alternate" type="text/html" title="Building TUIs with Dart has never been easier" /><published>2026-06-05T15:00:00+02:00</published><updated>2026-06-05T15:00:00+02:00</updated><id>https://roszkowski.dev/2026/dart-tui-nocterm</id><content type="html" xml:base="https://roszkowski.dev/2026/dart-tui-nocterm/"><![CDATA[<p>Ever since <a href="https://github.com/Norbert515">Norbert</a> announced <a href="https://nocterm.dev/">Nocterm</a>, I wanted to use it for myself. I played a bit at first, but only recently with the help of LLMs and amazing documentation I was able to build something usable.</p>

<h1 id="disk_analyzer_cli">disk_analyzer_cli</h1>

<p>I first started with simple <a href="https://pub.dev/packages/disk_analyzer_cli">disk_analyzer</a> utility. It scans the disk and shows big folders and files.</p>

<table>
  <thead>
    <tr>
      <th>Main window</th>
      <th>Treemap</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><img src="/images/disk_analyzer_1.png" alt="" /></td>
      <td><img src="/images/disk_analyzer_2.png" alt="" /></td>
    </tr>
  </tbody>
</table>

<p>I guess I need to drop _cli now as it’s a TUI.</p>

<pre><code class="language-sh">dart pub global activate disk_analyzer_cli
# or
brew tap orestesgaolin/tap
brew install disk_analyzer_cli
</code></pre>

<h1 id="git_chain">git_chain</h1>

<p>I often chain my PRs and have a few scripts to synchronize them all. However, when I worked with my colleagues in the same chain, I didn’t want them to have to go through that same process. So I spinned up nocterm again and built a <a href="https://pub.dev/packages/git_chain">git chain synchronization tool</a>. GitHub is working on a proper UI for that called <a href="https://github.github.com/gh-stack/">GitHub Stacked PRs</a>.</p>

<table>
  <thead>
    <tr>
      <th>Before sync</th>
      <th>During sync</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><img src="/images/git_chain_1.png" alt="" /></td>
      <td><img src="/images/git_chain_2.png" alt="" /></td>
    </tr>
  </tbody>
</table>

<pre><code class="language-sh">dart pub global activate git_chain
# or
brew tap orestesgaolin/tap
brew install git_chain
</code></pre>

<h1 id="git_branches">git_branches</h1>

<p>Another little tool called <a href="https://pub.dev/packages/git_branches">git_branches</a> resulted from me having to delete over 100 stale branches. We merge branches to main with squashing so they just linger in my local git workspace. I didn’t want to remove them blindly, so simple pick and choose TUI was the best solution.</p>

<table>
  <thead>
    <tr>
      <th>Main screen</th>
      <th>Delete screen</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><img src="/images/git_branches_1.png" alt="" /></td>
      <td><img src="/images/git_branches_2.png" alt="" /></td>
    </tr>
  </tbody>
</table>

<p>Next up… merge conflict resolution tool maybe?</p>

<pre><code class="language-sh">dart pub global activate git_branches
# or
brew tap orestesgaolin/tap
brew install git_branches
</code></pre>

<h1 id="nocterm">Nocterm</h1>

<p>If you haven’t tried yet, give <a href="https://github.com/Norbert515/nocterm">nocterm</a> a try.</p>]]></content><author><name>Dominik Roszkowski</name><email>dominik@roszkowski.dev</email></author><category term="flutter" /><category term="tui" /><category term="nocterm" /><category term="ai" /><summary type="html"><![CDATA[Nocterm is revolutionizing the way I built tools for myself]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roszkowski.dev/images/disk_analyzer_2.png" /><media:content medium="image" url="https://roszkowski.dev/images/disk_analyzer_2.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Everyone is building the same thing…</title><link href="https://roszkowski.dev/2026/everybody-builds-the-same/" rel="alternate" type="text/html" title="Everyone is building the same thing…" /><published>2026-05-26T15:00:00+02:00</published><updated>2026-05-26T15:00:00+02:00</updated><id>https://roszkowski.dev/2026/everybody-builds-the-same</id><content type="html" xml:base="https://roszkowski.dev/2026/everybody-builds-the-same/"><![CDATA[<p>Lately, it looks like everybody is trying to build more or less the same thing. AI-enabled mobile testing and building tools. Too many to try and these are just the ones that I’m aware of:</p>

<ul>
  <li><a href="https://tester.army/">Tester Army</a> - announced just today</li>
  <li><a href="https://github.com/callstackincubator/agent-device">agent-device</a> - pretty fun project from <a href="https://x.com/thymikee">@thymikee</a> that seems to be basis for <a href="https://agent-device.dev/cloud">their SaaS</a></li>
  <li><a href="https://qampanion.com/">Qampanion</a> - seeing their ads everywhere</li>
  <li><a href="https://autosana.ai/">Autosana</a></li>
  <li><a href="https://mobai.run/">MobAI</a> - seems like webview based desktop app</li>
  <li><a href="https://maestro.dev/cloud">Maestro Cloud</a> - I used it before both locally and in the cloud, they seem to be reinventing few things towards the agentic approach</li>
  <li><a href="https://argent.swmansion.com/">Argent</a> - saw this on Twitter a few times</li>
  <li><a href="https://simdeck.sh/">SimDeck</a> from NativeScript</li>
  <li><a href="https://leancode.co/blog/patrol-mcp-release">Patrol MCP</a> and <a href="https://github.com/leancodepl/marionette_mcp">Marionete MCP</a> from LeanCode</li>
  <li><a href="https://quashbugs.com/">Quash</a></li>
  <li><a href="https://github.com/mobile-next/mobile-mcp">Mobile MCP</a> - played with that a bit</li>
  <li><a href="https://qa.tech/">QA.tech</a></li>
</ul>

<p>I ported agent-device to Dart as <a href="https://github.com/orestesgaolin/agent-device-dart">agent-device-dart</a>, but still not happy with the performance. The intention is to have a CLI tool that agent can use to run the app, but in the end it can also generate standard Dart tests (<a href="https://github.com/orestesgaolin/agent-device-dart/blob/main/packages/agent_device/test/platforms/android/fixture_app_live_test.dart">example</a>) to be run as part of the CI.</p>

<p>The <em>dream</em> ✨ is to have <strong>mostly automated mobile development flow</strong>. Either have a QA tool that AI agent can operate to validate in-app flows, or a dev tool that allows AI agent to iterate on a mobile app feature end-to-end.</p>

<p>One big problem I’m seeing is the flakiness of the underlying “plumbing”.</p>

<p>Accessibility tooling in simulators and devices is not reflective of the actual VoiceOver capabilities. Not every device feature is enabled on Android emulator or iOS simulator. Emulators and debug builds are slow.</p>

<p>The other way is to hook up to a given framework’s utilities, like Flutter’s VM service (e.g. what patrol is doing) or React Native’s dev tools (metro), but that is not cross-platform and also not really reflective of the real user experience e.g. in release mode.</p>

<p>I’m watching this space and would love to see something that can work well across emulators and real devices, that is accessible to both QA and devs, and that is able to deal with the flakiness of mobile E2E testing.</p>

<p>Have you solved it? If so, please share it.</p>]]></content><author><name>Dominik Roszkowski</name><email>dominik@roszkowski.dev</email></author><category term="flutter" /><category term="ai" /><category term="qa" /><summary type="html"><![CDATA[Tooling for AI agents for running mobile apps]]></summary></entry><entry><title type="html">jnigen and swiftgen in 2026 - some lessons learned</title><link href="https://roszkowski.dev/2026/swiftgen-jnigen/" rel="alternate" type="text/html" title="jnigen and swiftgen in 2026 - some lessons learned" /><published>2026-05-18T10:00:00+02:00</published><updated>2026-05-18T10:00:00+02:00</updated><id>https://roszkowski.dev/2026/swiftgen-jnigen</id><content type="html" xml:base="https://roszkowski.dev/2026/swiftgen-jnigen/"><![CDATA[<p>Package <a href="https://pub.dev/packages/jni">jni 1.0.0</a> was recently published. It’s a good opportunity to share some of my lessons from working with native interop over the last few years.</p>

<p>There were some breaking changes between jni 0.14/0.15 and 1.0.0, so if you were using jni and jnigen in the past, you may face some extra work when upgrading. This article was written after I got annoyed with the AI agent incorrect output when working on the migration.</p>

<h1 id="migration-to-jni100">Migration to jni@1.0.0</h1>

<p>I found that the quickest way to do it was to comment out all my native integration code first, just to rebuild the Android project and regenerate bindings afterwards.</p>

<p>Some APIs have changed (back) to more reasonable names, e.g., <code>toDartString()</code> is now <code>toString()</code>, or methods for setting properties became <strong>setters</strong> (<code>_service.onReplyListener(_listener!);</code> is now <code>_service.onReplyListener = _listener!;</code>). There are also some annoying changes like constructor overrides changing numbers, e.g., <code>Intent.new$2</code> can now be <code>Intent.new$12</code>…</p>

<p>Moreover, the new recommended way to define bindings configuration is <strong>no longer a yaml file</strong>, but rather a simple Dart script. I quite enjoy this new approach, as it hides the magic behind the code generation, and you can just add some pre- and post-processing logic to the generated code. It seems that the convention will be to put these into the <code>tool/</code> directory and run them with <code>dart run tool/jnigen.dart</code> or <code>dart run tool/swiftgen.dart</code>.</p>

<p>You can see example migration commits for simple Flutter apps:</p>

<ul>
  <li>from 0.15 to 1.0.0 <a href="https://github.com/orestesgaolin/native_interop_presentation/commit/4403c186696b199c91bcf24584c43b375f0e3d14">here</a>.</li>
  <li>from 0.14 to 1.0.0 <a href="https://github.com/orestesgaolin/native_interop_presentation/commit/cdb0dba7baf13a9304d9e8d28bdf3630f032d072">here</a></li>
</ul>

<p>Below I share some of the lessons I learned while working with jnigen and swiftgen in last few years. In the last few months I managed to update some of my packages:</p>

<ul>
  <li><a href="https://github.com/orestesgaolin/screen_brightness_monitor/tree/main">screen_brightness_monitor</a></li>
  <li><a href="https://pub.dev/packages/play_in_app_update/versions/1.1.0-pre.1">play_in_app_update</a></li>
  <li><a href="https://github.com/orestesgaolin/uikit_bindings">uikit_bindings</a></li>
  <li><a href="https://github.com/orestesgaolin/native_interop_presentation/tree/main/android_intent">android_intent</a></li>
  <li><a href="https://github.com/orestesgaolin/native_interop_presentation/tree/main/foreground_service_interop_plugin">foreground_service_interop</a></li>
</ul>

<h1 id="jnigen-android">jnigen (Android)</h1>

<p>Quick recap:</p>

<ul>
  <li>You can use jnigen to generate Dart bindings for both explicit and compiled Java/Kotlin code.</li>
  <li>Before generating the bindings you need to build the Android app at least once.<sup id="fnref:building" role="doc-noteref"><a href="#fn:building" class="footnote" rel="footnote">1</a></sup></li>
  <li>You can include both project-specific classes, as well as Android SDK types.</li>
  <li>There are some built-in helpers for common Android utilities, which now have been migrated to <a href="https://pub.dev/packages/jni_flutter">package:jni_flutter</a>.</li>
</ul>

<h2 id="generator-script-tooljnigendart">Generator script (<code>tool/jnigen.dart</code>)</h2>

<p>Example generator script for a plugin with two classes and one callback interface (callback pattern explained in the next section):</p>

<pre><code class="language-dart">import 'dart:io';
import 'package:jnigen/jnigen.dart';

void main(List&lt;String&gt; args) {
  final packageRoot = Platform.script.resolve('../');
  generateJniBindings(Config(
    outputConfig: OutputConfig(
      dartConfig: DartCodeOutputConfig(
        path: packageRoot.resolve('lib/src/my_plugin.g.dart'),
        structure: OutputStructure.singleFile,
      ),
    ),
    androidSdkConfig: AndroidSdkConfig(
      addGradleDeps: true,
      androidExample: 'example',     // Points to example app for Gradle resolution
    ),
    sourcePath: [packageRoot.resolve('android/src/main/java/')],
    classes: [
      'com.example.MyCallback',       // List ALL classes to bind
      'com.example.MyPlugin',
    ],
  ));
}
</code></pre>

<h2 id="callbacks-use-kotlin-interfaces">Callbacks: use Kotlin interfaces</h2>

<p>If you want to receive data back from native code to Dart, the approach I found is to define a callback interface. Then on the Dart side you can wrap it with more user-friendly API like <code>StreamController</code>. Often, it’s sufficient to just expose the callback directly.</p>

<p>Define a dedicated Kotlin interface. jnigen generates a type-safe <code>implement()</code> method and <code>$Mixin</code>:</p>

<pre><code class="language-kotlin">// Generates BrightnessCallback.implement($BrightnessCallback(...))
@Keep
interface BrightnessCallback {
    @Keep
    fun onBrightnessChanged(brightness: Int)
}
</code></pre>

<p>Then in Dart:</p>

<pre><code class="language-dart">final callback = BrightnessCallback.implement(
  $BrightnessCallback(
    onBrightnessChanged: (brightness) { /* ... */ },
    onBrightnessChanged$async: true,  // Non-blocking (listener pattern)
  ),
);
native.startObserving(callback);
</code></pre>

<h2 id="getting-access-to-context-and-activity---packagejni_flutter">Getting access to Context and Activity - package:jni_flutter</h2>

<p>There are some small changes to accessing Android Context (<code>androidApplicationContext</code>) and current Activity. It’s now part of the <code>package:jni_flutter</code>. Moreover, to get the current Activity, you need to pass the current <code>engineId</code>.</p>

<pre><code class="language-dart">final engineId = PlatformDispatcher.instance.engineId;
if (engineId == null) {
  print('Error: Engine ID is null');
  return;
}

final activity = androidActivity(engineId);
if (activity == null) {
  print('Error: Activity is null');
  return;
}
activity.as(a.Activity.type).startActivityForResult(intent, 1);
</code></pre>

<h2 id="casting">Casting</h2>

<p>To cast <code>JObject</code> onto a desired type, use <code>as()</code> with the generated type:</p>

<pre><code class="language-dart">final brightnessMonitor = native.getBrightnessMonitor();
final brightness = brightnessMonitor.as(ScreenBrightnessMonitor.type).brightness;
</code></pre>

<h2 id="arrays">Arrays</h2>

<p>It’s a bit cumbersome, but once you know, you know:</p>

<pre><code class="language-dart">final array = JArray.of&lt;JString&gt;(JString.type, ["cc@example.com".toJString()]);
</code></pre>

<h2 id="the-async-true-flag">The <code>$async: true</code> Flag</h2>

<p>I found that I’m basically always using <code>async: true</code> for callbacks. There’s some dedicated <a href="https://github.com/dart-lang/native/blob/main/pkgs/jnigen/doc/threading.md">threading documentation</a>, but not sure how up-to-date it is.</p>

<h2 id="annotate-with-keep">Annotate with <code>@Keep</code></h2>

<p>ProGuard/R8 strips unreferenced classes. Annotate every class, interface, property, and method that jnigen binds:</p>

<pre><code class="language-kotlin">@Keep
class ScreenBrightnessMonitor(private val context: Context) {
    @get:Keep          // For Kotlin properties, use @get:Keep
    val brightness: Int
        get() = /* ... */

    @Keep
    fun startObserving(callback: BrightnessCallback) { /* ... */ }
}
</code></pre>

<h2 id="issues-while-regenerating-bindings">Issues while regenerating bindings</h2>

<p>Sometimes, despite changing Kotlin code and rebuilding with gradle, the jnigen generator may throw errors like <code>Unexpected end of input (at character 1)</code>. In my case, the workaround is to rerun the Gradle build without cache.</p>

<pre><code class="language-bash">cd android
./gradlew :your_plugin_name:assembleDebug --no-daemon --console=plain --refresh-dependencies --rerun-tasks
cd ..
dart run tool/jnigen.dart
</code></pre>

<h2 id="memory-management">Memory management</h2>

<p>When using jni bindings, you have to remember about native Java objects that are getting referenced on each instantiation.</p>

<p>The overall assumption is that you don’t have to manually manage them. Once all references (in both Java and Dart) to an object are gone, Java’s garbage collector (GC) can reclaim it. Similarly, JObjects attach a native finalizer to their global references. Therefore, when the Dart GC collects them, the underlying Java reference is released.</p>

<p>However, sometimes you may want to control the lifecycle of objects more explicitly. Read more on reference management in the <a href="https://github.com/dart-lang/native/blob/main/pkgs/jnigen/doc/lifecycle.md">dedicated documentation</a>. To manually release a JNI global reference, call <code>.release()</code> on the Dart side:</p>

<pre><code class="language-dart">void dispose() {
  native.stopObserving();
  callback?.release();
  native.release();
}
</code></pre>

<h1 id="swiftgen-ios">swiftgen (iOS)</h1>

<p>Swiftgen is still not stable, but I’ve had some success using it so far. The Swift code needs to be compatible with Objective-C, and swiftgen handles the bridging to Dart via <code>swift2objc</code> and <code>ffigen</code>.</p>

<p>I have published <a href="https://pub.dev/packages/screen_brightness_monitor">package:screen_brightness_monitor</a> that uses swiftgen for iOS bindings.</p>

<h2 id="generator-script-toolswiftgendart">Generator script (<code>tool/swiftgen.dart</code>)</h2>

<p>Similarly to jnigen, I recommend using a Dart script for configuration. Here’s an example for a plugin with one class and one callback protocol:</p>

<pre><code class="language-dart">import 'dart:io';
import 'package:ffigen/ffigen.dart' as fg;
import 'package:logging/logging.dart';
import 'package:swiftgen/swiftgen.dart';

Future&lt;void&gt; main() async {
  final logger = Logger('swiftgen');
  logger.onRecord.listen((record) {
    stderr.writeln('${record.level.name}: ${record.message}');
  });

  final packageRoot = Platform.script.resolve('../');

  // Resolve SDK path/version manually:
  final sdkPath = (await Process.run('xcrun', [
    '--sdk', 'iphoneos', '--show-sdk-path',
  ])).stdout.toString().trim();
  final sdkVersion = (await Process.run('xcrun', [
    '--sdk', 'iphoneos', '--show-sdk-version',
  ])).stdout.toString().trim();

  await SwiftGenerator(
    target: Target(
      triple: 'arm64-apple-ios$sdkVersion',
      sdk: Uri.directory(sdkPath),
    ),
    inputs: [
      ObjCCompatibleSwiftFileInput(
        files: [
          packageRoot.resolve('ios/Classes/MyWidget.swift'),
        ],
      ),
    ],
    output: Output(
      module: 'my_plugin',
      dartFile: packageRoot.resolve('lib/src/my_plugin_ios.g.dart'),
      objectiveCFile: packageRoot.resolve('ios/Classes/my_plugin.m'),
    ),
    ffigen: FfiGeneratorOptions(
      objectiveC: fg.ObjectiveC(
        interfaces: fg.Interfaces(
          include: (decl) =&gt; decl.originalName == 'MyWidget',
        ),
        protocols: fg.Protocols(
          include: (decl) =&gt; decl.originalName == 'MyCallback',
        ),
      ),
    ),
  ).generate(logger: logger);
}
</code></pre>

<h3 id="objccompatibleswiftfileinput-vs-swiftfileinput"><code>ObjCCompatibleSwiftFileInput</code> vs <code>SwiftFileInput</code></h3>

<ul>
  <li><strong><code>SwiftFileInput</code></strong>: For pure Swift code. swift2objc wraps it in ObjC-compatible wrappers.</li>
  <li><strong><code>ObjCCompatibleSwiftFileInput</code></strong>: For Swift code that’s <strong>already <code>@objc</code> annotated</strong>. Skips the wrapping step – simpler, fewer surprises. <strong>Prefer this when you control the Swift code.</strong></li>
</ul>

<h2 id="writing-objc-compatible-swift">Writing ObjC-compatible Swift</h2>

<p>All types exposed to Dart must be <code>@objc</code> annotated and inherit from <code>NSObject</code> (for classes):</p>

<pre><code class="language-swift">// Protocol — callback interface
@objc public protocol BrightnessCallback {
    @objc func onBrightnessChanged(_ brightness: Int)
}

// Class — must inherit NSObject
@objc public class ScreenBrightnessMonitor: NSObject {
    @objc public override init() { super.init() }

    @objc public var brightness: Int { /* ... */ }

    @objc public func startObserving(callback: BrightnessCallback) { /* ... */ }
    @objc public func stopObserving() { /* ... */ }
}
</code></pre>

<p><strong>Rules:</strong></p>

<ul>
  <li>Classes must inherit <code>NSObject</code> (direct or indirect).</li>
  <li>Use <code>@objc public</code> on everything ffigen should see.</li>
  <li>Overriding <code>init()</code> requires <code>override</code> + calling <code>super.init()</code>.</li>
  <li>Only ObjC-compatible types work: <code>Int</code>, <code>String</code>, <code>Bool</code>, <code>NSObject</code> subclasses, protocols. No Swift structs, enums with associated values, or generics.</li>
</ul>

<h2 id="ffigen-include-filters">ffigen include filters</h2>

<p>By default, ffigen generates bindings for <strong>everything</strong> in the ObjC header. Use <code>include</code> filters to limit output to your types only:</p>

<pre><code class="language-dart">ffigen: FfiGeneratorOptions(
  objectiveC: fg.ObjectiveC(
    interfaces: fg.Interfaces(
      include: (decl) =&gt; decl.originalName == 'ScreenBrightnessMonitor',
    ),
    protocols: fg.Protocols(
      include: (decl) =&gt; decl.originalName == 'BrightnessCallback',
    ),
  ),
),
</code></pre>

<p>Without filters, you’ll get bindings for <code>NSObject</code>, <code>NSString</code>, etc. – hundreds of unnecessary lines.</p>

<h3 id="implementing-objc-protocols-in-dart">Implementing ObjC protocols in Dart</h3>

<p>swiftgen/ffigen generates three flavors for each protocol:</p>

<table>
  <thead>
    <tr>
      <th>Method</th>
      <th>Use When</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>implement(...)</code></td>
      <td>Callback runs synchronously, blocking the ObjC caller until Dart returns</td>
    </tr>
    <tr>
      <td><code>implementAsListener(...)</code></td>
      <td><strong>Callback is non-blocking</strong> – ObjC caller continues immediately (use for observers/notifications)</td>
    </tr>
    <tr>
      <td><code>implementAsBlocking(...)</code></td>
      <td>Callback blocks the ObjC thread and waits for Dart to complete</td>
    </tr>
  </tbody>
</table>

<p>For observer/notification patterns, use <code>implementAsListener</code>:</p>

<pre><code class="language-dart">final callback = BrightnessCallback$Builder.implementAsListener(
  onBrightnessChanged_: (brightness) {
    controller.add(brightness);
  },
);
native.startObservingWithCallback(callback);
</code></pre>

<h2 id="sdk-version-workaround">SDK version workaround</h2>

<p>When building my package, I found that <code>Target.iOSArm64Latest()</code> may crash with <code>FormatException</code> if swift2objc’s <code>_parseVersion</code> regex can’t parse your Xcode SDK version string. The workaround is to resolve the SDK path and version manually via <code>xcrun</code> and construct the <code>Target</code> directly (see generator script above). Perhaps I did something wrong?</p>

<h2 id="generated-files">Generated files</h2>

<p>swiftgen produces <strong>two</strong> files:</p>

<ol>
  <li><strong>Dart bindings</strong> (<code>lib/src/..._ios.g.dart</code>) – extension types wrapping ObjC objects</li>
  <li><strong>ObjC bindings</strong> (<code>ios/Classes/....m</code>) – C functions that ffigen’s Dart code calls via <code>dart:ffi</code></li>
</ol>

<p>Both must be committed. The <code>.m</code> file must be in a location picked up by the podspec (<code>Classes/**/*</code>).</p>

<h2 id="objc-method-names-ios">ObjC method names (iOS)</h2>

<p>Swift <code>func startObserving(callback:)</code> becomes <code>startObservingWithCallback:</code> in ObjC (and thus in the Dart binding). Check the generated <code>.g.dart</code> for actual method names.</p>

<h2 id="podspec-source_files">Podspec <code>source_files</code></h2>

<p>Must include both the Swift source and the generated <code>.m</code> file. <code>Classes/**/*</code> covers both.</p>

<h1 id="cross-platform-dart-wrapper">Cross-platform Dart wrapper</h1>

<p>Neither jnigen nor swiftgen will generate a single API for both platforms (as opposed to <code>package:pigeon</code>). Below I share a simple pattern I used in my plugin to expose a common API to users.</p>

<h2 id="abstract-class--factory-constructor">Abstract class + factory constructor</h2>

<p>Define an abstract class with a factory constructor that instantiates the correct platform implementation at runtime:</p>

<pre><code class="language-dart">// lib/src/brightness_monitor.dart
import 'dart:io' show Platform;
import 'brightness_monitor_android.dart';
import 'brightness_monitor_ios.dart';

abstract class BrightnessMonitor {
  factory BrightnessMonitor() {
    if (Platform.isAndroid) return BrightnessMonitorAndroid();
    if (Platform.isIOS) return BrightnessMonitorIos();
    throw UnsupportedError('Unsupported platform');
  }

  int get brightness;
  Stream&lt;int&gt; get onBrightnessChanged;
  void dispose();
}
</code></pre>

<p>Each platform file imports only its own generated bindings, so platform-specific <code>dart:ffi</code> symbols don’t conflict.</p>

<h2 id="keep-a-dart-side-reference-to-callbacks">Keep a Dart-side reference to callbacks</h2>

<p>Even with a strong Swift reference, store the callback object in a field (<code>_callback</code>) on the Dart side too. If it’s only a local variable in <code>_startObserving()</code>, the Dart GC can collect the closure backing the protocol proxy, breaking the callback silently. Clean it up in <code>_stopObserving()</code>:</p>

<pre><code class="language-dart">BrightnessCallback? _callback;

void _startObserving() {
  _callback = BrightnessCallback$Builder.implementAsListener(
    onBrightnessChanged_: (b) { _controller?.add(b); },
  );
  _native.startObservingWithCallback(_callback!);
}

void _stopObserving() {
  _native.stopObserving();
  _callback = null;
}
</code></pre>

<h1 id="android-vs-ios-api-comparison">Android vs iOS API comparison</h1>

<table>
  <thead>
    <tr>
      <th>Concern</th>
      <th>jnigen (Android)</th>
      <th>swiftgen (iOS)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Callback definition</strong></td>
      <td>Kotlin <code>interface</code></td>
      <td>Swift <code>@objc protocol</code></td>
    </tr>
    <tr>
      <td><strong>Callback creation</strong></td>
      <td><code>MyCallback.implement($MyCallback(...))</code></td>
      <td><code>MyCallback$Builder.implementAsListener(...)</code></td>
    </tr>
    <tr>
      <td><strong>Async/non-blocking</strong></td>
      <td><code>method$async: true</code> in <code>$Mixin</code></td>
      <td><code>implementAsListener(...)</code> variant</td>
    </tr>
    <tr>
      <td><strong>Context/init</strong></td>
      <td>Pass <code>Context</code> via <code>Jni.androidApplicationContext</code></td>
      <td>No context needed; <code>init()</code> or default constructor</td>
    </tr>
    <tr>
      <td><strong>Memory</strong></td>
      <td><code>.release()</code> to free JNI global ref</td>
      <td>Automatic (ARC via ObjC runtime)</td>
    </tr>
    <tr>
      <td><strong>Native superclass</strong></td>
      <td>Any Java/Kotlin class</td>
      <td>Must extend <code>NSObject</code></td>
    </tr>
    <tr>
      <td><strong>Allowed types</strong></td>
      <td>Any JNI-compatible type</td>
      <td>ObjC-compatible types only (no Swift structs/generics)</td>
    </tr>
  </tbody>
</table>

<h1 id="final-words">Final words</h1>

<p>I’ve been using jni, jnigen, ffi, and swiftgen for over a year now. The setup experience requires some effort, but once you have the bindings ready, it’s pretty smooth sailing<sup id="fnref:sailing" role="doc-noteref"><a href="#fn:sailing" class="footnote" rel="footnote">2</a></sup>. I wish the docs included more examples, though. I hope this short article will help others get started faster and give future AI models a bit more native-interop content in their corpus.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:building" role="doc-endnote">
      <p>Seems like it will be possible to skip <code>flutter build apk</code> as per <a href="https://github.com/dart-lang/native/pull/3303">this PR</a> and run simply <code>flutter pub get</code>. <a href="#fnref:building" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:sailing" role="doc-endnote">
      <p>I’m not a sailor, though. <a href="#fnref:sailing" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Dominik Roszkowski</name><email>dominik@roszkowski.dev</email></author><category term="flutter" /><category term="dart" /><category term="native interop" /><category term="jni" /><category term="swiftgen" /><summary type="html"><![CDATA[jni 1.0 was recently published. It's a good opportunity to share some of my lessons from working with native interop over the last few years.]]></summary></entry><entry><title type="html">Binding to Android Foreground Service with JNI and jnigen from Flutter</title><link href="https://roszkowski.dev/2026/jni-foreground-service/" rel="alternate" type="text/html" title="Binding to Android Foreground Service with JNI and jnigen from Flutter" /><published>2026-02-22T13:20:00+01:00</published><updated>2026-02-22T13:20:00+01:00</updated><id>https://roszkowski.dev/2026/jni-foreground-service</id><content type="html" xml:base="https://roszkowski.dev/2026/jni-foreground-service/"><![CDATA[<p>The more I work with jni and native APIs from Flutter, the more I think it’s time to move away from methods channels entirely. In this short post I’m going to show how to bind to Android’s foreground service directly from Dart, and send messages back and forth without additional plugin class.</p>

<p>Here’s what I built:</p>

<ul>
  <li>starting and binding to a foreground service from Flutter</li>
  <li>requesting and checking for notification permissions directly from Dart</li>
  <li>sending messages from Flutter to foreground service</li>
  <li>receiving messages from foreground service notification as if it was a messaging app</li>
</ul>

<table>
<tr>
<td width="50%">
<video loop="" autoplay="" width="100%" playsinline="" muted="">
    <source src="/assets/images/android-fs-flutter.mp4" type="video/mp4" />
</video>
</td>
<td width="50%">
<video loop="" autoplay="" width="100%" playsinline="" muted="">
    <source src="/assets/images/android-fs-rebinding.mp4" type="video/mp4" />
</video>
</td>
</tr>
</table>

<p>I first started with <a href="https://github.com/orestesgaolin/native_interop_presentation/commit/2acb2cf32d01b09f91500a39333c81660c32cd07">more traditional approach of having a plugin class</a> that proxied calls to the foreground service, but after some chats with my friend we realized we can skip this step and bind directly to the service from Flutter.</p>

<p>Let’s recap some useful helpers included in jni:</p>

<ul>
  <li><code>Jni.androidApplicationContext</code> - needed to start the service and send messages to it</li>
  <li><code>jni.Jni.androidActivity(engineId)</code> and <code>PlatformDispatcher.instance.engineId</code> - needed to request notification permissions</li>
</ul>

<h2 id="jnigen-configuration-2026">jnigen configuration 2026</h2>

<p>Currently the best way to configure jnigen is to use Dart script like following:</p>

<pre><code class="language-dart">import 'dart:io';

import 'package:jnigen/jnigen.dart';

void main(List&lt;String&gt; args) {
  final packageRoot = Platform.script.resolve('../');
  generateJniBindings(
    Config(
      outputConfig: OutputConfig(
        dartConfig: DartCodeOutputConfig(
          path: packageRoot.resolve(
            'lib/src/foreground_service_interop_plugin.g.dart',
          ),
          structure: OutputStructure.singleFile,
        ),
      ),
      androidSdkConfig: AndroidSdkConfig(
        addGradleDeps: true,
        androidExample: 'example',
      ),
      sourcePath: [packageRoot.resolve('android/src/main/java')],
      classes: [
        'com.example.foreground_service_interop_plugin.ExampleForegroundService',
        'com.example.foreground_service_interop_plugin.ReplyListenerProxy',
        'android.content.ServiceConnection',
        'android.content.Intent',
        'android.content.Context',
        'android.app.Activity',
        'androidx.core.content.ContextCompat',
        'androidx.core.app.ActivityCompat',
      ],
    ),
  );
}
</code></pre>

<p>On each native change you have to rebuild the example app with <code>flutter build apk</code> and then run this script to regenerate bindings via <code>dart tool/jnigen.dart</code>.</p>

<h2 id="permissions">Permissions</h2>

<p>Before you can spawn a foreground service, you need to request <code>POST_NOTIFICATIONS</code> permission on Android 13+.</p>

<p>For that we have to bind <code>androidx.core.content.ContextCompat</code> and <code>androidx.core.app.ActivityCompat</code>. Then the permissions can be requested and checked <strong>synchronously</strong>:</p>

<pre><code class="language-dart">void requestPermission() {
    final engineId = PlatformDispatcher.instance.engineId;
    if (engineId == null) {
        print('Engine ID is null, cannot request permission');
        return;
    }
    // This is how you cast the Android Activity from JNI
    final activity = jni.Jni.androidActivity(engineId)?.as(native.Activity.type);
    if (activity == null) {
        print('Activity is null, cannot request permission');
        return;
    }

    native.ActivityCompat.requestPermissions(
        activity,
        jni.JArray.of(jni.JString.type, ['android.permission.POST_NOTIFICATIONS'.toJString(),]),
        0,
    );

    // set timer to poll for permissions (up to 30 seconds)
    Timer.periodic(const Duration(seconds: 1), (timer) {
        final granted = checkPermission();
        if (granted) {
            setState(() {
                hasPermission = true;
            });
            timer.cancel();
        } else {
            print('Permission not granted yet, checking again...');
        }
    });
}

bool checkPermission() {
    final context = jni.Jni.androidApplicationContext.as(native.Context.type);

    final result = native.ContextCompat.checkSelfPermission(
        context,
        'android.permission.POST_NOTIFICATIONS'.toJString(),
    );
    return result == 0;
}

</code></pre>

<h2 id="starting-and-binding-to-foreground-service">Starting and binding to foreground service</h2>

<p>Having the permissions granted, we can start the foreground service and bind to it directly from Flutter. We’ll need to bind <code>android.content.Context</code> and <code>android.content.Intent</code> for that.</p>

<pre><code class="language-dart">void startAndBind() {
    final context = jni.Jni.androidApplicationContext.as(native.Context.type);
    final intent = native.Intent.new$1(
        context,
        native.ExampleForegroundService.type.jClass,
    );
    context.bindService$1(
        intent,
        serviceConnection,
        native.Context$BindServiceFlags.of(native.Context.BIND_AUTO_CREATE),
    );

    setState(() {});
}
</code></pre>

<p>When connecting to service a <code>android.content.ServiceConnection</code> is needed. We are going to implement it in a familiar way by passing <em>implementation</em> proxy object.</p>

<pre><code class="language-dart">@override
void initState() {
    super.initState();

    serviceConnection = native.ServiceConnection.implement(
        native.$ServiceConnection(
        onServiceConnected: (componentName, iBinder) {
            localBinder = iBinder?.as(native.ExampleForegroundService$LocalBinder.type);
            service = localBinder?.getService();

            // Converting proxy callback to stream subscription
            repliesSubscription = service?.replyStream.replies.listen((reply) {
                setState(() {
                    messages.add((DateTime.now(), 'Service', reply));
                });
            });

            setState(() {});
        },
        onServiceDisconnected: (componentName) {
            service = null;
            localBinder = null;
            repliesSubscription?.cancel();
            setState(() {});
        },
        onBindingDied: (componentName) {
            //
        },
        onNullBinding: (componentName) {
            //
        },
        ),
    );
}
</code></pre>

<p>This time instead of using callbacks via proxy, I created a wrapper to convert from them to stream (<code>service?.replyStream.replies</code>). You can check out the implementation here. This makes the API much more Dart-like in my opinion.</p>

<h2 id="sending-and-receiving-messages">Sending and receiving messages</h2>

<p>Once we have access to the service, we can invoke <code>receiveMessage</code> method to update the notification message. Messages from the notification are received via the <code>replyStream</code> as shown in the previous code snippet.</p>

<pre><code class="language-dart">void sendMessage() {
  if (service != null) {
    final msg = 'Hello from Flutter at ${DateTime.now()}';
    messages.add((DateTime.now(), 'Flutter', msg));
    service?.receiveMessage(msg.toJString());
  }
  setState(() {});
}
</code></pre>

<h2 id="final-thoughts">Final thoughts</h2>

<p>It’s really cool, what possibilities are now open with jni and jnigen. Step-by-step, it was possible to access Android’s foreground service API directly from Dart without any method channels or plugin classes. The API is pretty clean and Dart-like, especially with some wrapper classes to convert callbacks to streams.</p>

<p class="notice--info"><a href="https://github.com/orestesgaolin/native_interop_presentation/tree/main/foreground_service_interop_plugin">Click here for the full source code</a>.</p>

<p>Final tips:</p>

<ol>
  <li>It is very important to cast JObject using <code>as(ClassName.type)</code> extension method rather than Dart’s <code>as</code> keyword.</li>
  <li>On each change to the native code, make sure to run <code>flutter build apk</code> in the example code and then regenerate bindings with <code>dart tool/jnigen.dart</code>.</li>
</ol>

<h2 id="thoughts-about-llms-and-jnigen">Thoughts about LLMs and jnigen.</h2>

<p>From my experience, current AI models are not really up to date with jnigen possibilities. So here’s my little contribution - let LLMs feed on this data and use <em>my</em> way to access native code in Flutter ;)</p>]]></content><author><name>Dominik Roszkowski</name><email>dominik@roszkowski.dev</email></author><summary type="html"><![CDATA[If you're writing any native integration in Flutter in 2026, you should probably use jni and jnigen by this time.]]></summary></entry><entry><title type="html">JNI and jnigen: Access Android In-App Update from Flutter</title><link href="https://roszkowski.dev/2025/android-in-app-update/" rel="alternate" type="text/html" title="JNI and jnigen: Access Android In-App Update from Flutter" /><published>2025-12-25T16:20:00+01:00</published><updated>2025-12-25T16:20:00+01:00</updated><id>https://roszkowski.dev/2025/android-in-app-update</id><content type="html" xml:base="https://roszkowski.dev/2025/android-in-app-update/"><![CDATA[<p>I’ve just published new package using JNI bindings for <a href="https://developer.android.com/guide/playcore/in-app-updates">Play Core In-App Update API</a>. It lets you trigger flexible or immediate updates straight from Dart code without the usual method channel boilerplate.</p>

<p><a href="https://github.com/orestesgaolin/play_core_libraries/tree/main/in_app_update">Repository</a></p>

<p><a href="https://pub.dev/packages/play_in_app_update">package: play_in_app_update</a>.</p>

<p><img src="https://raw.githubusercontent.com/orestesgaolin/play_core_libraries/refs/heads/main/in_app_update/doc/screenshot_app_update.gif" alt="" /></p>

<p>It is a thin layer over Play Core native libs generated with <code>jnigen</code>, so you call the same API you would in Kotlin, just from Dart. Works on Android 5.0+ (SDK 21) and supports both update types. See <a href="https://github.com/orestesgaolin/play_core_libraries/blob/main/in_app_update/tool/jnigen.dart">jnigen.dart</a> for details how it’s been generated.</p>

<p>Example app includes a simple UI to trigger updates and shows how to listen to update state changes. You can check the README for testing and usage instructions.</p>

<p>Besides low-level access to the API that lets you use Android’s docs as if they were written for Dart, there’s also a more Flutter-friendly high-level API that uses Dart idioms (see <a href="https://github.com/orestesgaolin/play_core_libraries/blob/main/in_app_update/lib/wrapper.dart">wrapper.dart</a> and corresponding <a href="https://github.com/orestesgaolin/play_core_libraries/blob/main/in_app_update/example/lib/wrapper_page.dart">example code</a>).</p>

<p>If you end up shipping it in production and notice any rough edges, feel free to create issue.</p>]]></content><author><name>Dominik Roszkowski</name><email>dominik@roszkowski.dev</email></author><summary type="html"><![CDATA[Just published JNI bindings to Play Core In-App Update API on Android]]></summary></entry><entry><title type="html">Using Android Intent via native interop with jnigen</title><link href="https://roszkowski.dev/2025/android-jnigen/" rel="alternate" type="text/html" title="Using Android Intent via native interop with jnigen" /><published>2025-07-22T16:20:00+02:00</published><updated>2025-07-22T16:20:00+02:00</updated><id>https://roszkowski.dev/2025/android-jnigen</id><content type="html" xml:base="https://roszkowski.dev/2025/android-jnigen/"><![CDATA[<p>In this post I’ll show how to access some of the Android APIs directly in Dart/Flutter. The final result will be a plugin allowing to send some basic Intents to the Android system, like opening the e-mail client or selecting a contact.</p>

<center>
<video loop="" autoplay="" width="50%" playsinline="" muted="">
  <source src="/assets/images/jnigen-email.mp4" type="video/mp4" />
  <source src="/assets/images/jnigen-email.webm" type="video/webm" />
</video>
</center>

<p>We wanted to open user’s default e-mail client to send a properly formatted message, but the typical <code>package:url_launcher</code> approach of creating a <code>mailto:</code> was too flaky. Android <a href="https://developer.android.com/guide/components/intents-common.html#Email">allows to provide a specific intent</a> of type <code>ACTION_SENDTO</code> that will query the system for the best app to handle it. I created bindings for this Intent call with jnigen and here’s a bit of overview.</p>

<h2 id="getting-started">Getting started</h2>

<p>Create new Flutter plugin and modify the <code>pubspec.yaml</code> and <code>jnigen.yaml</code> to match the example in the <a href="https://github.com/dart-lang/native/tree/main/pkgs/jnigen/example">jnigen repository</a>. This will only work on Android, hence:</p>

<pre><code class="language-bash">flutter create --template plugin --platforms android android_intent
cd android_intent/example
flutter build apk --release
</code></pre>

<p>After cleaning up the code and dependencies <strong>make sure to build your Android example app in release</strong>. You can also open it in Android Studio to check if everything is correct.</p>

<p>This setup assumes following package versions and works as of July 2025:</p>

<pre><code>jnigen: ^0.14.0
jni: ^0.14.2
</code></pre>

<h2 id="jnigen-setup">jnigen setup</h2>

<p>The basic setup of jnigen plugin works fine for me although it results with over 20k generated lines of Dart code. To be able to access Android APIs you need to specify all the classes (and their dependencies e.g. <code>android.net.Uri</code>) that would normally be imported in Java/Kotlin code. The following <code>jnigen.yaml</code> file worked for me:</p>

<pre><code class="language-yaml">android_sdk_config:
  add_gradle_deps: true
  android_example: "example/"

source_path:
  - "android/src/main/java"
classes:
  - "android.content.Intent"
  - "android.content.Context"
  - "android.app.Activity"
  - "android.net.Uri"

output:
  dart:
    path: "lib/src/bindings.dart"
    structure: "single_file"
</code></pre>

<p>Once you run the jnigen code generator, you should be able to use the generated bindings directly in Dart code.</p>

<pre><code class="language-sh">flutter pub run jnigen --config jnigen.yaml
</code></pre>

<h2 id="usage-in-dart">Usage in Dart</h2>

<p>To send a simple Intent to open the e-mail client, you can use the following Dart code:</p>

<pre><code class="language-dart">void sendEmail() {
  try {
    final intent = Intent.new$2(Intent.ACTION_SENDTO);
    intent.setData(Uri.parse("mailto:".toJString()));

    intent.putExtra$21(
      Intent.EXTRA_EMAIL,
      JArray.of&lt;JString&gt;(JString.type, ["example@example.com".toJString()]),
    );
    intent.putExtra$8(Intent.EXTRA_SUBJECT, "Subject".toJString());
    intent.putExtra$8(Intent.EXTRA_TEXT, "Body text".toJString());
    intent.putExtra$21(
      Intent.EXTRA_CC,
      JArray.of&lt;JString&gt;(JString.type, ["cc@example.com".toJString()]),
    );
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    Context contextInstance = Context.fromReference(Jni.getCachedApplicationContext());
    contextInstance.startActivity(intent);
  } catch (e) {
    print('Error opening email: $e');
  }
}
</code></pre>

<p>What is quite cool it follows 1:1 the code from the <a href="https://developer.android.com/guide/components/intents-common#Email">Android documentation</a>:</p>

<pre><code class="language-kotlin">fun composeEmail(addresses: Array&lt;String&gt;, subject: String) {
    val intent = Intent(Intent.ACTION_SENDTO).apply {
        data = Uri.parse("mailto:") // Only email apps handle this.
        putExtra(Intent.EXTRA_EMAIL, addresses)
        putExtra(Intent.EXTRA_SUBJECT, subject)
    }
    if (intent.resolveActivity(packageManager) != null) {
        startActivity(intent)
    }
}
</code></pre>

<p>Obviously there are several slightly annoying differences:</p>

<ul>
  <li>You have to cast Dart types to Jni types, e.g. <code>toJString()</code>.</li>
  <li>You have to use <code>JArray.of&lt;JString&gt;()</code> to create an array of strings</li>
  <li>You have to use <code>putExtra$21</code> and <code>putExtra$8</code> methods as Dart does not support method overloading</li>
</ul>

<p>On the flipside, the <a href="https://pub.dev/packages/jni">package:jni</a> comes with few convenience methods:</p>

<ul>
  <li><code>Jni.getCachedApplicationContext()</code> to get the application context which I can cast to <code>Context</code> and use it to start the intent.</li>
  <li>not shown here <code>Jni.getCurrentActivity()</code></li>
</ul>

<h2 id="sending-intents-with-results">Sending Intents with results</h2>

<p>It isn’t directly possible to receive Intent results via Activity.onActivityResult(), so I had to come up with a workaround. The main inspiration was <a href="https://pub.dev/packages/receive_intent">package:receive_intent</a>, and my previous attempt of using <a href="https://github.com/orestesgaolin/native_interop_presentation/blob/main/appupdate/android/src/main/kotlin/dev/roszkowski/appupdate/InstallStateUpdatedListenerProxy.kt">proxy with a listener interface</a>. There may be other ways in the future to achieve this – see <a href="https://github.com/dart-lang/native/issues/2435">this issue</a> for details.</p>

<p>Anyway, to receive intent results like this, you need to override the <code>onActivityResult</code> of your main Activity.</p>

<pre><code class="language-kotlin">const val REQUEST_SELECT_CONTACT = 1

fun selectContact() {
    val intent = Intent(Intent.ACTION_PICK).apply {
        type = ContactsContract.Contacts.CONTENT_TYPE
    }
    if (intent.resolveActivity(packageManager) != null) {
        startActivityForResult(intent, REQUEST_SELECT_CONTACT)
    }
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
    if (requestCode == REQUEST_SELECT_CONTACT &amp;&amp; resultCode == RESULT_OK) {
        val contactUri: Uri = data.data
        // Do something with the selected contact at contactUri.
    }
}
</code></pre>

<p><code>FlutterPlugin</code> comes with a <code>ActivityAware</code> interface that allows you to register a result listener via <code>ActivityPluginBinding</code>:</p>

<pre><code class="language-kotlin">override fun onAttachedToActivity(binding: ActivityPluginBinding) {
    activity = WeakReference(binding.activity)
    binding.addActivityResultListener { res, req, intent -&gt;
        Log.i("ARLP", "addActivityResultListener called with intent: $intent")
    }
}
</code></pre>

<p>So to make that work I had to implement a typical Flutter plugin class that would keep <code>OnResultListener</code> interface. This interface will later be implemented in Dart.</p>

<p>This also means that now my plugin includes a Kotlin class of its own so it needs to be registered in the <code>pubspec.yaml</code> file:</p>

<pre><code class="language-diff">flutter:
  plugin:
    platforms:
      android:
        ffiPlugin: true
+        package: com.example.android_intent
+        pluginClass: ActivityResultListenerProxy
</code></pre>

<p>Next up I created a Kotlin class <code>ActivityResultListenerProxy</code>:</p>

<pre><code class="language-kotlin">@Keep
class ActivityResultListenerProxy : FlutterPlugin, ActivityAware {
    private var activity: WeakReference&lt;Activity&gt;? = null;
    private var callback: OnResultListener? = null;

    public interface OnResultListener {
        fun onResult(requestCode: Int, resultCode: Int, result: String?)
    }

    @Keep
    public fun onResult(requestCode: Int, resultCode: Int, intent: Intent?) : Boolean {
        if (intent != null) {
            callback?.onResult(requestCode, resultCode, intent.dataString)
            return true
        } else {
            return false
        }
    }

    override fun onAttachedToActivity(binding: ActivityPluginBinding) {
        activity = WeakReference(binding.activity)
        binding.addActivityResultListener { res, req, intent -&gt;
            Log.i("ARLP", "onAttachedToActivity called with intent: $intent")
            onResult(res, req, intent)
        }
    }

    override fun onDetachedFromActivityForConfigChanges() {
        activity?.clear()
        activity = null
    }

// other methods
}
</code></pre>

<p>One challange was to get hold of the plugin instance that would be created by Flutter. To make it work I made it a singleton and added a static companion object that would hold the instance. Plugin cannot be Kotlin’s <code>object</code>.</p>

<pre><code class="language-kotlin">@Keep
class ActivityResultListenerProxy : FlutterPlugin, ActivityAware {
    private var callback: OnResultListener? = null;

    companion object {
        @Volatile
        private var instance: ActivityResultListenerProxy? = null

        fun getInstance(): ActivityResultListenerProxy {
            return instance ?: synchronized(this) {
                instance ?: ActivityResultListenerProxy().also { instance = it }
            }
        }
    }

    @Keep
    public fun setOnResultListener(listener: OnResultListener) {
        callback = listener
    }
// other methods
}
</code></pre>

<p>Then after updating my jnigen.yaml I was able to start intent with result.</p>

<pre><code class="language-yaml">android_sdk_config:
  add_gradle_deps: true
  android_example: "example/"

source_path:
  - "android/src/main/java"
classes:
  - "android.content.Intent"
  - "android.content.Context"
  - "android.app.Activity"
  - "android.net.Uri"
  - "android.provider.ContactsContract"
  - "com.example.android_intent.ActivityResultListenerProxy"

output:
  dart:
    path: "lib/src/bindings.dart"
    structure: "single_file"
</code></pre>

<p>This time I had to use <code>Activity</code> to start the intent and register the result listener.</p>

<pre><code class="language-dart">void selectContact() {
  try {
    final intent = Intent.new$2(Intent.ACTION_PICK);
    intent.setType(ContactsContract$Contacts.CONTENT_TYPE);

    listener = ActivityResultListenerProxy.Companion.getInstance();
    listener!.setOnResultListener(
      ActivityResultListenerProxy$OnResultListener.implement(
        $ActivityResultListenerProxy$OnResultListener(
          onResult: (result, request, data) {
            print('ARLP Selected contact: $result, request: $request, data: $data');
          },
        ),
      ),
    );

    final activity = Activity.fromReference(Jni.getCurrentActivity());

    activity.startActivityForResult(intent, 1);
  } catch (e) {
    print('Error selecting contact: $e');
  }
}
</code></pre>

<p>I experimented a bit more with other Intents like selecting an image from camera and they all seemed feasible. It would take a bit more API design to make it work, but nonethelss it’s quite cool to use Android APIs directly in Dart code.</p>

<p>You can find the full source code in my <a href="https://github.com/orestesgaolin/native_interop_presentation/tree/main/android_intent">native_interop repository</a>.</p>]]></content><author><name>Dominik Roszkowski</name><email>dominik@roszkowski.dev</email></author><summary type="html"><![CDATA[How to use jnigen to access Android Intents in Flutter app.]]></summary></entry><entry><title type="html">Dart macros are not coming</title><link href="https://roszkowski.dev/2025/dart-macros/" rel="alternate" type="text/html" title="Dart macros are not coming" /><published>2025-01-29T20:20:00+01:00</published><updated>2025-01-29T20:20:00+01:00</updated><id>https://roszkowski.dev/2025/dart-macros</id><content type="html" xml:base="https://roszkowski.dev/2025/dart-macros/"><![CDATA[<p><a href="https://medium.com/dartlang/an-update-on-dart-macros-data-serialization-06d3037d4f12">The news just dropped</a> that Dart will not get macros/metaprogramming/data classes support anytime soon. <a href="https://github.com/dart-lang/language/issues/314">The related ticket</a> to improve data class support in Dart received almost 1400 upvotes, which I think shows how important that issue has been for us Dart and Flutter developers. We’ve been impatiently waiting for macros for a while now. It was supposed to be a solution to all data handling and serialization problems. Now the work has been stopped and the explanation given by Vijay seems like it’s actually a good call.</p>

<p><img src="/images/2025-01-macros/macros.png" alt="Medium post announcing: Dart macros are not coming anytime soon..." /></p>

<p>Last year a first preview of macros support arrived and we all got quite excited. For me the most fun were improvements to <a href="https://github.com/felangel/equatable/issues/175">equatable</a> and json serialization. It looked promising and since then we were basically waiting for more updates.</p>

<p>The explanation we are given in the post paints a picture of much more complicated situation than might be expected. Seems like the rippling effect of the macros feedback loop would degrade the overall developer experience by slowing down the analysis and compilation times significantly — in a way that the Dart team could not find solutions for. I am known to complain about slow analyzer a lot (mostly due to big monorepo projects using hundreds of barrel files etc.) and even with recent <a href="https://dart.dev/tools/pub/workspaces">pub workspace</a> update it’s not really the smoothest ride. Hence, for me if the macros would make my work slower, I don’t think I’d be the most avid user of them.</p>

<p>Seems like the common code generation with build_runner is going to stay with us for much longer. I hope that at least some form of more convenient serialization is going to arrive — you can check <a href="https://github.com/schultek/codable">recent RFP from Kilian Schulte</a> (<a href="https://forum.itsallwidgets.com/t/rfc-new-serialization-protocol-for-dart/2355">discussion post</a>) that tries to solve this in a more generalized way than just focusing on json serialization only.</p>

<p>In the meantime stay safe!</p>]]></content><author><name>Dominik Roszkowski</name><email>dominik@roszkowski.dev</email></author><summary type="html"><![CDATA[Dart team just announced they will be stopping working on macros. I don't think it's that bad, though.]]></summary></entry><entry><title type="html">Cool Flutter packages: flutter_physics</title><link href="https://roszkowski.dev/2025/flutter-physics/" rel="alternate" type="text/html" title="Cool Flutter packages: flutter_physics" /><published>2025-01-27T08:20:00+01:00</published><updated>2025-01-27T08:20:00+01:00</updated><id>https://roszkowski.dev/2025/flutter-physics</id><content type="html" xml:base="https://roszkowski.dev/2025/flutter-physics/"><![CDATA[<p>Last week I discovered a pretty fun little animations package built by @sangddn that lets you replace classic Flutter animation controller with one that follows spring-based simulations: <a href="https://pub.dev/packages/flutter_physics">flutter_physics</a>.</p>

<p><a href="https://docs.flutter.dev/cookbook/animation/physics-simulation">The concept of simulation is present in Flutter</a>, but I’ve always found it a bit tricky to implement. I tried it few times, but eventually would switch to either custom calculations or basic curves.</p>

<center>
<video loop="" autoplay="" width="90%" playsinline="" muted="">
  <source src="/images/2025-01-flutter-physics/card.mp4" type="video/mp4" />
  <source src="/images/2025-01-flutter-physics/card.webm" type="video/mp4" />
</video>
</center>

<p>Normally when building this kind of pseudo-3d movement you wouldn’t use typical tweens and curves, as they lack inertia. With flutter_physics you can follow the same approach as with any animation, but just replace the controller with PhysicsController or TweenAnimationBuilder with PhysicsBuilder. In the example above the card starts to move more naturally and just feels better to the eye and finger.</p>

<p>One area where I feel the physics-based animation would be beneficial is bottom sheet movement. I think most of the implementations, including the Flutter bottom sheets, don’t feel <a href="https://github.com/flutter/flutter/issues/152587">as nice as some of the provided by iOS</a>. I’d love to change it and will experimented with using flutter_physics for sheets animations.</p>

<p>Here’s the behavior with ordinary Flutter AnimationController:</p>

<center>
<video loop="" autoplay="" width="90%" playsinline="" muted="">
  <source src="/images/2025-01-flutter-physics/before.mp4" type="video/mp4" />
  <source src="/images/2025-01-flutter-physics/before.webm" type="video/mp4" />
</video>
</center>

<p>And here after just replacing the controller with PhysicsController (via adapter - source code below):</p>

<center>
<video loop="" autoplay="" width="90%" playsinline="" muted="">
  <source src="/images/2025-01-flutter-physics/after.mp4" type="video/mp4" />
  <source src="/images/2025-01-flutter-physics/after.webm" type="video/mp4" />
</video>
</center>

<p>If you want to try something refreshing, play with the flutter_physics and maybe <a href="https://forum.itsallwidgets.com/t/share-your-feedback-on-animation/2277/1">share some feedback about how you approach animations with John Ryan here on the Flutter Forum</a>.</p>

<hr />

<p>Source code</p>

<pre><code class="language-dart">import 'package:flutter/material.dart';
import 'package:flutter_physics/flutter_physics.dart';

class AnimationsControllerAdapter implements AnimationController {
  AnimationsControllerAdapter(this.physicsController);

  final PhysicsController physicsController;

  @override
  Duration? get duration =&gt; physicsController.duration;

  @override
  set duration(Duration? value) {
    physicsController.duration = value;
  }

  @override
  Duration? get reverseDuration =&gt; physicsController.reverseDuration;

  @override
  set reverseDuration(Duration? value) {
    physicsController.reverseDuration = value;
  }

  @override
  double get value =&gt; physicsController.value;

  @override
  set value(double value) {
    physicsController.value = value;
  }

  @override
  void addListener(VoidCallback listener) {
    physicsController.addListener(listener);
  }

  @override
  void addStatusListener(AnimationStatusListener listener) {
    physicsController.addStatusListener(listener);
  }

  @override
  TickerFuture animateBack(double target,
      {Duration? duration, Curve curve = Curves.linear}) {
    return physicsController.animateTo(
      target,
      duration: duration,
      physics: curve,
    );
  }

  @override
  TickerFuture animateTo(double target,
      {Duration? duration, Curve curve = Curves.linear}) {
    return physicsController.animateTo(
      target,
      duration: duration,
      physics: curve,
    );
  }

  @override
  TickerFuture animateWith(Simulation simulation) {
    return physicsController.animateWith(simulation);
  }

  @override
  AnimationBehavior get animationBehavior =&gt;
      physicsController.animationBehavior;

  @override
  void clearListeners() {
    physicsController.clearListeners();
  }

  @override
  void clearStatusListeners() {
    physicsController.clearStatusListeners();
  }

  @override
  String? get debugLabel =&gt; physicsController.debugLabel;

  @override
  void didRegisterListener() {
    // TODO: implement didRegisterListener
  }

  @override
  void didUnregisterListener() {
    physicsController.didUnregisterListener();
  }

  @override
  void dispose() {
    physicsController.dispose();
  }

  @override
  Animation&lt;U&gt; drive&lt;U&gt;(Animatable&lt;U&gt; child) {
    return physicsController.drive(child);
  }

  @override
  TickerFuture fling(
      {double velocity = 1.0,
      SpringDescription? springDescription,
      AnimationBehavior? animationBehavior}) {
    return physicsController.fling(
      velocity: velocity,
      springDescription: springDescription,
      animationBehavior: animationBehavior,
    );
  }

  @override
  TickerFuture forward({double? from}) {
    return physicsController.forward(from: from);
  }

  @override
  bool get isAnimating =&gt; physicsController.isAnimating;

  @override
  bool get isCompleted =&gt; physicsController.isCompleted;

  @override
  bool get isDismissed =&gt; physicsController.isDismissed;

  @override
  bool get isForwardOrCompleted =&gt; physicsController.isForwardOrCompleted;

  @override
  Duration? get lastElapsedDuration =&gt; physicsController.lastElapsedDuration;

  @override
  double get lowerBound =&gt; physicsController.lowerBound;

  @override
  void notifyListeners() {
    physicsController.notifyListeners();
  }

  @override
  void notifyStatusListeners(AnimationStatus status) {
    physicsController.notifyStatusListeners(status);
  }

  @override
  void removeListener(VoidCallback listener) {
    physicsController.removeListener(listener);
  }

  @override
  void removeStatusListener(AnimationStatusListener listener) {
    physicsController.removeStatusListener(listener);
  }

  @override
  TickerFuture repeat(
      {double? min,
      double? max,
      bool reverse = false,
      Duration? period,
      int? count}) {
    return physicsController.repeat(
      min: min,
      max: max,
      reverse: reverse,
      period: period,
      count: count,
    );
  }

  @override
  void reset() {
    physicsController.reset();
  }

  @override
  void resync(TickerProvider vsync) {
    physicsController.resync(vsync);
  }

  @override
  TickerFuture reverse({double? from}) {
    return physicsController.reverse(from: from);
  }

  @override
  AnimationStatus get status =&gt; physicsController.status;

  @override
  void stop({bool canceled = true}) {
    physicsController.stop(canceled: canceled);
  }

  @override
  String toStringDetails() {
    return physicsController.toStringDetails();
  }

  @override
  TickerFuture toggle({double? from}) {
    throw UnimplementedError('toggle is not implemented yet');
  }

  @override
  double get upperBound =&gt; physicsController.upperBound;

  @override
  double get velocity =&gt; physicsController.velocity;

  @override
  Animation&lt;double&gt; get view =&gt; physicsController.view;
}

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

  @override
  State&lt;BottomSheetPage&gt; createState() =&gt; _BottomSheetPageState();
}

class _BottomSheetPageState extends State&lt;BottomSheetPage&gt;
    with TickerProviderStateMixin {
  late PhysicsController controller;

  @override
  void initState() {
    controller = PhysicsController(vsync: this);
    super.initState();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            showModalBottomSheet(
              context: context,
              transitionAnimationController:
                  AnimationsControllerAdapter(controller),
              builder: (context) {
                return Container(
                  height: 200,
                  color: Colors.blueGrey,
                  child: Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      mainAxisSize: MainAxisSize.min,
                      children: &lt;Widget&gt;[
                        const Text('Modal BottomSheet'),
                        ElevatedButton(
                          child: const Text('Close BottomSheet'),
                          onPressed: () =&gt; Navigator.pop(context),
                        ),
                      ],
                    ),
                  ),
                );
              },
            );
          },
          child: Text('Show bottom sheet'),
        ),
      ),
    );
  }
}
</code></pre>]]></content><author><name>Dominik Roszkowski</name><email>dominik@roszkowski.dev</email></author><summary type="html"><![CDATA[A package that made me excited about animations again :)]]></summary></entry><entry><title type="html">Native interop with Kotlin/Java in Flutter</title><link href="https://roszkowski.dev/2025/native-interop-with-jnigen/" rel="alternate" type="text/html" title="Native interop with Kotlin/Java in Flutter" /><published>2025-01-17T16:20:00+01:00</published><updated>2025-01-17T16:20:00+01:00</updated><id>https://roszkowski.dev/2025/native-interop-with-jnigen</id><content type="html" xml:base="https://roszkowski.dev/2025/native-interop-with-jnigen/"><![CDATA[<p>Last year during the Fluttercon session about future of native interop I learned about this new tool called <a href="https://pub.dev/packages/jnigen">jnigen</a>. It’s became quite stable recently and I wanted to try it out again.</p>

<h1 id="some-background">Some background</h1>

<p>In the past I used Xamarin and back then it was quite natural to invoke platform APIs from C# through something called bindings. In other words classes like <code>Activity</code>, <code>Context</code>, <code>NSUrl</code>, <code>UIView</code> were accessible directly from C# without any additional glue code. It was also possible with a bit of work to expose <a href="https://github.com/aloisdeniel/Xamarin.Bindings">most of the native libraries in a similar fashion</a>. Sort of similar thing is also available in <a href="https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-connect-to-apis.html">Kotlin Multiplatform</a> where you can access import namespaces like <code>platform.Foundation.NSUUID</code>. They even do some experiments with using <a href="https://touchlab.co/composeswiftbridge">SwiftUI views in Compose</a>.</p>

<p class="notice--info">See also my <a href="/2025/android-jnigen">new article</a> on using Android APIs from Dart.</p>

<h1 id="current-state-of-native-interop-in-dartflutter">Current state of native interop in Dart/Flutter</h1>

<p>Some might say that number of ways to interact with the platform from Dart is getting out of hand, sometimes leading to people choosing not to use native APIs at all or switch between random wrappers from pub.dev.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p>

<p>When interacting with Java/Kotlin/Swift classes in Flutter you need to go via plugin that either includes some glue code using platform channels, or can use some type-safe approach like pigeon or protobuf. Most of the plugins are wrappers around platform-specific APIs e.g. to show notifications or access shared preferences. For some more advanced scenarios you may want to look into <a href="https://docs.flutter.dev/platform-integration/android/c-interop">dart:ffi</a> but the learning curve is quite steep for a typical mobile developer.</p>

<p>Even with the simplest method channel you have to write very similar code 3 times: in the platform interface, in the implementation for each platform, and then handling of it on the native side. In my case I just write it manually (+LLM) as it’s typically faster than any of the code generation methods I tried before.</p>

<p>From what we know Flutter team is actively working on enabling <em>automatic</em> direct native interop, see the short video below for some recent updates from “Flutter in Production” event (Dec 2024):</p>

<iframe width="560" height="560" src="https://www.youtube.com/embed/PRo98xNvyjQ" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>
<!-- https://www.youtube.com/watch?v=PRo98xNvyjQ -->

<h1 id="jnigen">jnigen</h1>

<p>The promise of <a href="https://pub.dev/packages/jnigen">jnigen</a> is to generate Dart bindings from arbitrary Kotlin/Java code. How does it work:</p>

<blockquote>
  <p>jnigen scans compiled JAR files or Java source code to generate a description of the API, then uses it to generate Dart bindings. The Dart bindings call the C bindings, which in-turn call the Java functions through JNI. Shared functionality and base classes are provided through the support library, package:jni.</p>
</blockquote>

<p>It’s a bit roundabout way, but it feels more understandable than what I remember from Xamarin bindings.</p>

<h1 id="simple-example">Simple example</h1>

<p class="notice--info"><strong>Note:</strong> This is not meant to be a tutorial how to use jnigen, but rather an inspiration piece :)</p>

<p>A quite simple example can be found in the project repo where they call a suspend Kotlin function from Dart.</p>

<p>Given a simple Kotlin class:</p>

<pre><code class="language-kotlin">import androidx.annotation.Keep
import kotlinx.coroutines.*

@Keep
class Example {
  public suspend fun thinkBeforeAnswering(): String {
    delay(1000L)
    return "42"
  }
}
</code></pre>

<p>after some code generation you can simply instantiate it and invoke the function with in async-await fashion:</p>

<pre><code class="language-dart">final example = Example();
final answer = await example.thinkBeforeAnswering();
print(answer); // prints 42 after a while;
</code></pre>

<p>However, what is quite cool, you can also invoke functions synchronously. Given this Java class:</p>

<pre><code class="language-java">
package com.example.in_app_java;

import android.app.Activity;
import android.widget.Toast;
import androidx.annotation.Keep;

@Keep
public abstract class AndroidUtils {
  public static void showToast(Activity mainActivity, CharSequence text, int duration) {
    mainActivity.runOnUiThread(() -&gt; Toast.makeText(mainActivity, text, duration).show());
  }
}
</code></pre>

<p>A simplified invocation would look like this:</p>

<pre><code class="language-dart">import 'package:jni/jni.dart';

final activity = JObject.fromReference(Jni.getCurrentActivity());
AndroidUtils.showToast(activity, message.toJString(), 0);
</code></pre>

<p>As you might have noticed you get some handy helpers to retrieve the current activity or context.</p>

<p>If you ask me, this is exactly what I was looking for.</p>

<h1 id="example-with-platform-view">Example with platform view</h1>

<p>To play a bit more I wanted to rewrite some of actual production code from the app I’m working on to use jnigen instead of method channels. I’ve been using <a href="https://pub.dev/packages/flutter_pdfview">a fork of this pdf plugin</a><sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> that not only is a classic Flutter plugin, but also includes a platform view.</p>

<p>After getting rid of the platform channel calls I had to figure out how to control the <code>pdfView</code> instance that is hosted in the platform view. The simplest way I found was to keep it <a href="https://github.com/orestesgaolin/native_interop_presentation/blob/10b51ddf83a00d612a9d6cf276a2a1e61b978f13/pdf_viewer/packages/pdf/android/src/main/java/com/example/pdf/FlutterPDFView.java#L23">in a static field</a> that gets accessed through <a href="https://github.com/orestesgaolin/native_interop_presentation/blob/main/pdf_viewer/packages/pdf/android/src/main/java/com/example/pdf/PDFViewController.java">a god-like controller class</a>. <code>FlutterPDFView</code> is slightly modified platform view implementation from the original plugin.</p>

<pre><code class="language-java">@Keep
public class PDFViewController {
    public void setPage(int page){
        if (FlutterPDFView.pdfView == null){
            return;
        }

        FlutterPDFView.pdfView.jumpTo(page);
    }
// ...
}
</code></pre>

<p>Having my <code>PDFViewController</code> that can access the plugin’s platform view instance I generated the bindings with <code>jnigen</code> and was able to instantiate this class in Dart. What is super exciting is that when referencing the instance in Dart you get to see the exact same memory address as on the native side.</p>

<figure class="half ">
  
    
      <a href="/images/2025-01-plugins/dart-debug.png" title="Debugging the Dart class in VS Code">
          <img src="/images/2025-01-plugins/dart-debug.png" alt="Debugging the Dart class in VS Code" />
      </a>
    
  
    
      <a href="/images/2025-01-plugins/android-debug.png" title="Debugging the Java class in Android Studio">
          <img src="/images/2025-01-plugins/android-debug.png" alt="Debugging the Java class in Android Studio" />
      </a>
    
  
  
    <figcaption>Click to zoom into the screenshots from VS Code and Android Studio
</figcaption>
  
</figure>

<p>I needed a way to get notified about page changes from the native side. There’s no support for generating bindings for streams or similar communication channels, so you have to pass a listener that can include a callback to get invoked.</p>

<p>After some back and forth I learned that you can generate bindings for Java interfaces that later get implemented in Dart. It blew my mind. In other words I get to create a Dart class that in runtime is implementing a Java interface. This lets me pass it to my previously instantiated class and use it on the Java side.</p>

<p>Given <a href="https://github.com/orestesgaolin/native_interop_presentation/blob/main/pdf_viewer/packages/pdf/android/src/main/java/com/example/pdf/PDFStatusListener.java">this Java interface</a>:</p>

<pre><code class="language-java">@Keep
public interface PDFStatusListener {
    void onLoaded();
    void onPageChanged(int page, int total);
    void onError(String error);
    void onLinkRequested(String uri);
    void onDisposed();
}
</code></pre>

<p>You can instantiate the implementation of it in Dart:</p>

<pre><code class="language-dart">final listener = pdf.PDFStatusListener.implement(
    pdf.$PDFStatusListener(
        onLoaded$async: true,
        onPageChanged$async: true,
        onError$async: true,
        onLinkRequested$async: true,
        onDisposed$async: true,
        onLoaded: () {
            print('PDF Loaded');
        },
        onPageChanged: (int? page, int? total) {
            print('PDF page: $page, total: $total');
        },
        onError: (JString? string) {
            print('PDF error: ${string?.toDartString()}');
        },
        onLinkRequested: (JString? string) {
            print("Link: ${string?.toDartString()}");
        },
        onDisposed: () {
            print('PDF disposed');
            example.release();
        },
    ),
);
example.setPdfStatusListener(listener);
</code></pre>

<p>Being able to pass an instance of class as listener allows me to convert it to any form I like whether it’s a Dart stream or just a void callback. It still requires writing some wrappers and glue code, but somehow it feels more valuable than repetitive platform channels.</p>

<h1 id="what-about-swift">What about Swift</h1>

<p>There’s experimental bindings generator for Swift: <a href="https://github.com/dart-lang/native/tree/swiftgen2/pkgs/swiftgen">swiftgen</a>. I tried it only once, but will definitely try to reproduce the same example and perhaps share my findings here.</p>

<p>Hope you enjoyed this and perhaps you’ll try it for yourself. Truth is, without Flutter developers getting interested, there’s no real incentive to push forward.</p>

<p>Cheers!</p>

<hr />

<p>Other resources:</p>

<ul>
  <li>Follow Hossein for most up-to-date news about native interop: https://x.com/YousefiDash</li>
  <li><a href="https://pub.dev/packages/jnigen">jnigen</a></li>
  <li><a href="https://github.com/dart-lang/native/tree/main/pkgs/swiftgen">swiftgen</a> - may require branch change</li>
  <li><a href="https://www.droidcon.com/2024/09/02/the-past-present-and-future-of-native-interop/">The past, present, and future of native interop</a> - Dart team talk about native interop e.g. JNIgen</li>
  <li><a href="https://www.droidcon.com/2024/10/17/future-of-dart-panel/">Future of Dart panel</a></li>
  <li><a href="https://github.com/dart-lang/native/milestone/13">SwiftGen GH project</a></li>
</ul>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>I’ve heard several time how people were switching between various audio player packages just because of some missing features or little bugs, notification plugins, camera implementations etc. I wish we wouldn’t have to go through this “pick and choose” flow with fundamental capabilities like media playback or basic OS functionalities. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>I keep a private fork that uses different implementation on iOS and builds well with my app. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Dominik Roszkowski</name><email>dominik@roszkowski.dev</email></author><summary type="html"><![CDATA[Learn how to use jnigen to generate Kotlin/Java bindings for Flutter plugins.]]></summary></entry><entry><title type="html">Reflecting on Flutter after Fluttercon 2024</title><link href="https://roszkowski.dev/2024/fluttercon/" rel="alternate" type="text/html" title="Reflecting on Flutter after Fluttercon 2024" /><published>2024-07-05T12:00:00+02:00</published><updated>2024-07-05T12:00:00+02:00</updated><id>https://roszkowski.dev/2024/fluttercon</id><content type="html" xml:base="https://roszkowski.dev/2024/fluttercon/"><![CDATA[<p><img src="/images/2024/fluttercon_2024_pic.jpg" alt="Fluttercon 2024" /></p>

<p>Not sure how about you, but lately I’ve been getting impression that Flutter ecosystem has been affected by some pessimistic or defeatist claims, sometimes even overshadowing other good news or experiments. Thankfully, coming again to a big event makes you realize that online chatter is not the whole picture.</p>

<p>If you’re not in the twitters and reddits of the web, here’s a quick recap of the most impactful voices either commenting or responding to claims that Flutter can be killed by Google:</p>

<ul>
  <li><a href="https://developers.googleblog.com/en/making-development-across-platforms-easier-for-developers/">Joint blog note from Directors of Android and Flutter on selecting the toolkit for mobile apps</a>, note that Jetpack Compose Multiplatform is not mentioned</li>
  <li><a href="https://ln.hixie.ch/?start=1714717681&amp;count=1">How big is the Flutter team</a> by Hixie, who’s no longer at Google, but still actively contributing</li>
  <li><a href="https://www.reddit.com/r/FlutterDev/comments/1cduhra/comment/l1j9eoo/">Layoffs at Google affecting Flutter</a> - viral post that put focus on Flutter</li>
  <li><a href="https://x.com/MiSvTh/status/1785767966815985893">Response from Michael Thomsen, Flutter PM</a>, where he tries to cut the discussion</li>
</ul>

<p>Same as many of you I sometimes get a bit of FUD, but in the long run, I see how the general trajectory of the framework and ecosystem maintains stable. To support this, let’s recap some of the exiting news and showcases from this year’s Fluttercon.</p>

<h1 id="platform-views-and-the-big-thread-merging">Platform Views and the big thread merging</h1>

<p>The Flutter TL John McCutchan had a quite insightful talk about Platform Views on Android. It was refreshing to see the team admitting to mistakes in the current implementation, as well as proposing a long-term plan of fixing that. The only issue is that these fixes will only be enabled by the upcoming Android SDKs (34 and 35) which means that we’ll have to deal with current limitations for another year or two. Aside from that <a href="https://github.com/flutter/flutter/issues/150525#issuecomment-2180988248">they confirmed that there’s a plan on merging platform and Flutter UI thread</a>, which sparked a lot of discussion e.g. around native interop (things like method channels, ffi, JNI).</p>

<p>Folks from the Dart team have shown the <code>jnigen</code> tool that enables calling Java and Kotlin code directly from Dart, avoiding message passing through method channels. In short you could potentially invoke Kotlin plugin functions through simple Dart interface like in <a href="https://github.com/dart-lang/native/blob/main/pkgs/jnigen/example/notification_plugin/example/lib/main.dart">this example</a>:</p>

<pre><code class="language-dart">void showNotification(String title, String text) {
  i = i + 1;
  var jTitle = JString.fromString(title);
  var jText = JString.fromString(text);
  Notifications.showNotification(activity, i, jTitle, jText);
  jTitle.release();
  jText.release();
}
</code></pre>

<p>Some experiments are in progress to bring <a href="https://github.com/dart-lang/native/tree/main/pkgs/swiftgen">similar syntax to Swift through ObjC</a>.</p>

<p>Personally, I find this to be a really solid response to <a href="https://kotlinlang.org/docs/multiplatform-expect-actual.html#rules-for-expected-and-actual-declarations">Kotlin Multiplatform <code>expect</code> and <code>actual</code> declarations</a>.</p>

<h1 id="visuals-animations-shaders">Visuals, animations, shaders</h1>

<p>As it often happens, the most impressive talks are the ones that play with visuals. I got impressed <a href="https://x.com/LucasGoldner/status/1808800834051846180">by Raouf Rahiche’s take on fragment shaders</a>, reminding me of <a href="https://www.droidcon.com/2023/08/07/shaders-beyond-the-gimmick/">last year’s shader talk by Renan Araujo</a> (missed you!).</p>

<p>Btw check out the new project bringing some amazing shaders straight to Flutter: <a href="https://fluttershaders.com/">fluttershaders.com</a></p>

<h1 id="compose">Compose</h1>

<p>I went to couple of Compose and Compose Multiplatform talks just to see what makes the Android devs excited. Seems like Compose is doing well, although I can’t get rid of the uncanny feeling that it’s taking best from Flutter while keeping entire Android toolchain weight to deal with. Worth keeping an eye on Jetbrains efforts on Multiplatform as they are facing quite similar challenges as Flutter, while being able to avoid some of the dead ends that Flutter had to face (e.g. with multi window support, native interop).</p>

<h1 id="social-battery-charged">Social battery charged</h1>

<p>During my talk I asked folks what are the types of the apps that they work on and the response from over 200 people who scanned the QR code makes me feel joyful.</p>

<p><img src="/images/2024/word_cloud.png" alt="word cloud" /></p>

<p>To all the people I met and ones I missed - you’re doing amazing job, and I couldn’t be more happy to be part of this community. Thanks to all the organizers, speakers, and attendees for making this event so special. I’m looking forward to the next one!</p>]]></content><author><name>Dominik Roszkowski</name><email>dominik@roszkowski.dev</email></author><summary type="html"><![CDATA[Second edition of Fluttercon is done. It was such a good event. Let's reflect on Flutter and Dart while it's still fresh memory.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roszkowski.dev/images/2024/fluttercon_2024_pic.jpg" /><media:content medium="image" url="https://roszkowski.dev/images/2024/fluttercon_2024_pic.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>