Guides

Adding an AI agent to a Flutter app, and what GenUI gives you

The platform-channel integration, and an honest comparison with Flutter's own GenUI SDK: what the alpha covers, what production needs, and which to pick.

VVoqal · Engineering8 min readPart of Integration

About this article

Flutter is the one platform where this decision has a first-party answer, and that answer is still marked alpha. Anyone adding an agent to a Flutter app in 2026 is choosing between Google’s own GenUI SDK and a plugin over the native SDKs, and the tradeoff is specific enough to be worth a whole section.

The integration itself is short either way. What follows is the plugin route, using ours as the worked example, then the comparison nobody has written down.

Four calls out, one callback back. The surface the user sees is drawn by the native SDK, outside the Flutter widget tree.

How the channel works, in one paragraph#

Flutter talks to native code over platform channels. A MethodChannel carries named method calls, the standard codec handles “efficient binary serialization of simple JSON-like values, such as booleans, numbers, Strings, byte buffers, Lists, and Maps”, and results come back asynchronously. One rule catches people out: “whenever you invoke a channel method, you must invoke that method on the platform’s main thread.” A plugin handles that for you. A hand-rolled channel is where you meet it at 11pm.

The install#

The plugin is consumed as a git dependency pinned to a release tag:

yaml
dependencies:
  voqal_flutter:
    git:
      url: https://github.com/VoqalAI/voqal-flutter.git
      ref: "2.0.1"

Two pieces of platform setup. On Android the host activity has to be a FlutterFragmentActivity rather than a FlutterActivity, because the biometric prompt needs a ComponentActivity:

kotlin
import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity : FlutterFragmentActivity()

On iOS, a 16.0 platform floor in the Podfile and a microphone usage description in Info.plist. Skip the usage string and the first tap fails without a visible error.

Configure, then present#

dart
import 'package:voqal_flutter/voqal_flutter.dart';

final voqal = Voqal();

await voqal.setup(const VoqalConfig(
  apiKey: 'pk_live_…',
  requestId: 'prod-yourtenant',
  theme: VoqalTheme(accent: '#2d5bff', appearance: VoqalAppearance.auto),
));

await voqal.setCredentials(
  yourAuthToken,
  metadataJson: '{"country_code":"EGY","user_id":"123"}',
);

await voqal.prewarm();

setCredentials is the call that matters most. The token is your user’s own credential, and the agent inherits exactly its permissions, so it can never reach data that user could not reach by tapping through the app. Call it again whenever the token rotates rather than once at start.

Then open it from a button of your own:

dart
Scaffold(
  floatingActionButton: FloatingActionButton.extended(
    onPressed: () => voqal.present(),
    icon: const Icon(Icons.mic),
    label: const Text('Ask'),
  ),
  body: …,
)

The plugin ships no launcher widget. Placement belongs to whoever designed your navigation, and a bundled floating button would land on top of something in most apps.

The comparison: GenUI or native#

Neither lane is strictly better. The top one matches your Flutter theme exactly and moves with an alpha API; the bottom one is stable and draws outside your widget tree.

Flutter’s GenUI SDK is, in its own words, “an orchestration layer” that “coordinates the flow of information between your user, your Flutter widgets, and an AI agent”, using a JSON format to compose UI from your existing widget catalog and feeding user interactions back to the agent. That is the right architecture, and it is first-party, and the page carries an experimental label with a plain warning: “The genui package is in alpha and is likely to change.” The documentation was last updated on 19 August 2026.

What the alpha gives you is composition from your own widgets, which means the agent’s answers look exactly like the rest of your app because they are made of the same components. What it leaves with you is the catalog itself, the theming of every shape an answer can take, and the churn of an alpha API on a surface your users touch.

What a plugin over the native SDKs gives you is a fixed widget set that already exists, a confirm gate that already exists, and an API that is not going to move under you. What it costs you is that the surface is drawn in UIKit and Android views rather than Flutter, so it inherits your theme tokens rather than your widget code.

Pick GenUI if the assistant must sit inline in a Flutter screen and share components with it, and you can absorb an alpha dependency. Pick the plugin if the assistant is a modal surface, you ship on both platforms, and anything it does can write.

React Native teams face a narrower version of the same choice, with no first-party equivalent to GenUI. Adding an agent to a React Native app covers it.

What the fixed widget set actually contains#

The model picks from this list rather than writing layout. A closed set is what stops an agent from being talked into drawing a fake approval screen.

The closed set is a security property as much as a design one. An agent that can emit arbitrary layout can, in principle, be persuaded by injected content to draw something that looks like your approval UI. Choosing from a list removes that whole class of problem, and it is the main reason we have not moved to free-form generation. Why the catalogue stays closed is the longer version of that argument.

Adding another language#

Language is a configuration value rather than a second code path. The agent replies in whatever the user speaks, and the widget set mirrors itself for right-to-left scripts without you laying out a second screen.

What is not free is knowing whether recognition holds for the varieties your users speak. Published zero-shot results for Arabic put the same model near 15% word error on the written standard and near 79% on one regional variety, which is the gap between a working feature and an unusable one. Measuring what a speech model does on the varieties your users actually speak is the procedure to run before you ship.

Handing the user back to your own screens#

An agent that can only answer inside its own surface eventually hits a task it should not finish. Checkout is the usual one: the conversation gets the basket right, and then the user needs your real payment screen with your real payment sheet.

The plugin handles that with an action button beside the voice wave and a callback with no payload, so the destination is yours to decide:

dart
await voqal.setup(VoqalConfig(
  apiKey: 'pk_live_…',
  actionButtonEnabled: true,
));

voqal.onActionButtonTapped = () {
  Navigator.of(context).push(
    MaterialPageRoute(builder: (_) => const CheckoutPage()),
  );
};

Designing that handoff is worth more thought than it usually gets. The user has just had a conversation and is now in a normal screen, so whatever state the conversation established has to already be applied when they arrive. A checkout page that opens empty after a two-minute spoken order is worse than no agent at all.

Things that will bite you#

Using FlutterActivity instead of FlutterFragmentActivity compiles, runs, and then fails the first time a biometric prompt is needed. The prompt requires a ComponentActivity host, and nothing warns you until the moment it matters.

Forgetting the iOS microphone usage string produces no error message, just a tap that does nothing. Test the first tap on a real device rather than the simulator, where audio behaviour is cleaner than any real room.

Calling setCredentials once at startup works until the token expires, at which point every turn fails in a way that looks like a network problem. Wire it to your refresh path.

Presenting while another route transition is in flight can drop the call. Await the navigation first.

Testing the two platforms separately#

One plugin over two native SDKs means two sets of platform behaviour underneath one Dart API, and they do not fail in the same ways.

On Android, the host activity requirement is the first thing to verify, and the Voqal Maven repository has to be declared alongside it. Biometric behaviour also varies by device and by the user’s enrolled methods, so test on hardware rather than an emulator when you reach the confirm path.

On iOS, the microphone permission and the 16.0 floor are the two that bite, and both fail quietly. A simulator will happily present the assistant with no working audio input, which makes an audio problem look like a model problem.

Run the same spoken question set on both platforms before you ship. The agent is the same on the server, so any difference in outcome is a client or an audio difference, and knowing which of the two you are looking at saves an afternoon.

Where the real work is#

The client integration above is under an hour on an app that already has an auth token, which is roughly where a day of integration actually lands on any of the four platforms. After that, the project is on your backend: naming the operations the agent may call, writing error text it can recover from, deciding which of them may write, and designing what the confirm card shows before anything happens. Connecting it to the backend you already run covers that half, and the whole stack this sits in puts both halves in context.

One last honest note on speed. Our warm turns land around 2.5 to 3 seconds end to end against a live tenant, and a cold one can run far longer, dominated by the round trip to that tenant’s own services rather than by anything in Dart. prewarm exists to move that cost to app launch where nobody is waiting on it.

Sources#

Filed underIntegrationFlutterAgentsRender spec

Next

The quickstart wires the SDK into an app and runs one real turn against your own backend.

Open the Flutter quickstart

The rest of Integration

Open the cluster

iOS, Android, Flutter, React Native and web, from the first install to the first real turn against your own backend.

Elsewhere on the map