Welcome to Encatch Docs
Mobile & Native SDKs

Flutter SDK

Complete integration guide for the Encatch Flutter SDK — in-app feedback and survey collection for Flutter apps

The Encatch Flutter SDK lets you collect in-app feedback and surveys in Flutter apps. Display forms as a modal WebView overlay or inline in your layout, identify users, track screens and events, and submit responses to the Encatch backend.


Overview


Installation

flutter pub add encatch_flutter
dependencies:
  encatch_flutter: ^1.1.2

Quick Start

1. Initialization

Wrap your app's root widget with EncatchProvider to initialize the SDK, start a session, and mount the headless EncatchWebView listener for modal forms. No navigator key or extra WebView widget is required.

import 'package:encatch_flutter/encatch_flutter.dart';

void main() {
  runApp(
    EncatchProvider(
      apiKey: 'your-api-key',
      child: MyApp(),
    ),
  );
}

For inline forms, mount EncatchInlineForm in your screen widget tree separately.

Pass an optional EncatchConfig to customize SDK behavior:

EncatchProvider(
  apiKey: 'your-api-key',
  config: EncatchConfig(
    theme: EncatchTheme.system,
    debugMode: true,
    isFullScreen: false,
    apiBaseUrl: 'https://app.encatch.com',
    appVersion: '1.2.3',
    onBeforeShowForm: (payload) async {
      // Return false to prevent form from showing
      return true;
    },
  ),
  child: MyApp(),
)

Prop

Type

2. Identify users

Identify the current user. The userName is required (can be a username, email, or unique identifier). Traits and options are optional.

Username format

userName must be an ASCII identifier: 1–50 characters, using only letters A–Z / a–z, digits 0–9, and ., _, @, -. Spaces and non-English characters (Unicode, accented letters, emoji, etc.) are not supported. Use an email address, internal user ID, or ASCII username — for example user@example.com or user_123. To store a display name in another language, pass it as a trait instead (e.g. set: {'display_name': '…'}).

await Encatch.identifyUser('user@example.com');
await Encatch.identifyUser(
  'user@example.com',
  traits: UserTraits(
    set: {'name': 'Alice', 'plan': 'team'},
  ),
);
await Encatch.identifyUser(
  'user@example.com',
  traits: UserTraits(
    set: {'name': 'Alice', 'plan': 'team'},
    setOnce: {'firstSeen': DateTime.now()},
    increment: {'loginCount': 1},
    decrement: {'credits': 5},
    unset: ['trialEndDate'],
  ),
);

Prop

Type

User traits support the following operations:

OperationDescription
setSet user attributes (overwrites existing values)
setOnceSet user attributes only if they don't already exist
incrementIncrement numeric user attributes
decrementDecrement numeric user attributes
unsetRemove user attributes

Recommended

Using the secure option with a server-generated signature is recommended to verify that identification requests come from your backend. Keep your secret key on the server only — never expose it in client-side code.

Pass a server-generated HMAC signature so Encatch can validate the request. generatedDateTimeinUTC must be milliseconds since the Unix epoch (for example the string form of DateTime.now().millisecondsSinceEpoch from your server). When your publishable key has a session timeout, use the same value in HMAC-SHA256(userName + epochMs, secretKey). It is sent as the X-User-Signature-Time header and limits the signature's lifespan.

await Encatch.identifyUser(
  'user@example.com',
  options: IdentifyOptions(
    secure: SecureOptions(
      signature: 'your-hmac-signature',
      generatedDateTimeinUTC: '1741867200000', // ms since epoch (2025-03-13T12:00:00Z)
    ),
  ),
);

3. Show a form manually

Show a specific form by slug or ID.

await Encatch.showForm('feedback-form');
await Encatch.showForm('feedback-form', options: ShowFormOptions(
  reset: ResetMode.always,
));

Prop

Type

Prop

Type

ResetModeBehavior
ResetMode.alwaysReset pre-fill and response data on every form display
ResetMode.onCompleteReset only after the form is completed
ResetMode.neverNever reset response data

Pass caller context when showing a form:

await Encatch.showForm('feedback-form', options: ShowFormOptions(
  reset: ResetMode.always,
  context: {'plan': 'team', 'feature': 'checkout'},
));

Other actions

Set the user's preferred language.

Encatch.setLocale('fr');

Set the user's country.

Encatch.setCountry('FR'); // ISO 3166 country code

Set the theme for forms and surveys.

Encatch.setTheme(EncatchTheme.dark);
Encatch.setTheme(EncatchTheme.light);
Encatch.setTheme(EncatchTheme.system); // Follows system preference

await Encatch.trackEvent('button_clicked');

await Encatch.trackScreen('HomeScreen');

Add EncatchNavigatorObserver for automatic screen tracking:

MaterialApp(
  navigatorObservers: [EncatchNavigatorObserver()],
  // ...
)

Subscribe to form lifecycle events. Returns an unsubscribe function.

final unsubscribe = Encatch.on((eventType, payload) {
  print('Event: $eventType, payload: ${payload.data}');
});

// Later, to unsubscribe:
unsubscribe();
EventDescription
EventType.formShowFired when a form is displayed
EventType.formStartedFired when a user starts interacting
EventType.formSubmitFired when a form is submitted
EventType.formCompleteFired when a form is fully completed
EventType.formCloseFired when a form is closed
EventType.formDismissedFired when a form is dismissed without completion
EventType.formErrorFired when an error occurs
EventType.formSectionChangeFired when the visible section changes
EventType.formAnsweredFired when a question is answered
EventType.formRemindMeLaterFired when the user taps "Remind me later"
EventType.formCtaTriggeredFired when a completion CTA is triggered on thank-you or exit screens

Handle completion CTAs (in-app navigation, internal redirect, or external browser) via formCtaTriggered. Configure actions in the form builder — see Call to action. The SDK closes the form overlay after emitting the event — your app handles in-app navigation:

Encatch.on((eventType, payload) {
  if (eventType != EventType.formCtaTriggered) return;

  final action = payload.data?['action'];
  if (action != 'app_navigate') return;

  final route = payload.data?['route'] as String?;
  // Map route strings to your app's navigation paths
  if (route == 'billing' || route == 'billing/upgrade') {
    navigatorKey.currentState?.pushNamed('/billing');
  }
});

For in-app navigation, the SDK closes the form overlay after emitting the event and expects your app to perform navigation. For URL redirect actions, the SDK opens the URL and closes the form automatically.

Pre-fill a form response before showing a form. questionId may be a question UUID or a question slug.

Encatch.addToResponse('question_id', 'pre-filled value');
Encatch.addToResponse('choice_question_id', ['option-a', 'option-b']);

await Encatch.showForm('your-form-slug');

Dismiss the currently displayed form.

await Encatch.dismissForm();
// Or dismiss a specific form configuration:
await Encatch.dismissForm(formConfigurationId: 'config-id');

Use onBeforeShowForm in EncatchConfig to conditionally block forms from showing.

EncatchProvider(
  apiKey: 'your-api-key',
  config: EncatchConfig(
    onBeforeShowForm: (payload) async {
      // Inspect payload.formId, payload.formConfig, payload.triggerType, etc.
      if (payload.triggerType == TriggerType.automatic && someCondition) {
        return false; // Block this form
      }
      return true; // Allow
    },
  ),
  child: MyApp(),
)

EncatchProvider starts a session automatically after initialization. You can also control session lifecycle manually:

await Encatch.startSession();

// Skip the immediate ping or screen re-track on start:
await Encatch.startSession(
  options: StartSessionOptions(
    skipImmediatePing: true,
    skipImmediateTrackScreen: true,
  ),
);
// Temporarily stop the 30-second background ping (not persisted)
Encatch.pauseSession();

// Resume the ping interval after pauseSession()
Encatch.resumeSession();
// Fully suspend SDK activity — stops ping and dismisses open forms.
// Persists across app restarts. Re-enable with startSession().
await Encatch.stopSession();

Reset the current user identity and clear persisted identity data. Reverts the SDK to anonymous mode. User identity is preserved across stopSession() — use resetUser() after logout.

await Encatch.resetUser();

Wipes all persisted SDK data and resets in-memory state. Stronger than resetUser() — also clears session-stopped state and device preferences. The SDK remains initialized; call identifyUser afterward.

await Encatch.clearAll();

The SDK sends a background ping every 30 seconds (configurable via server response) to maintain engagement sessions and check for triggered forms. Ping is suppressed while a form is visible.


Inline Forms

Inline forms are a way to show Encatch in-app feedback without a modal — the survey renders directly in your layout instead of as a full-screen overlay.

EncatchInlineForm renders a form directly inside your widget tree instead of as a full-screen modal overlay. Place it anywhere — in a Column, SingleChildScrollView, Card, etc.

// In your screen's widget tree:
SingleChildScrollView(
  child: Column(
    children: [
      // ... content above ...
      EncatchInlineForm(
        formId: 'your-form-slug', // exact match; omit for wildcard
        enabled: ModalRoute.of(context)?.isCurrent ?? true,
      ),
      // ... content below ...
    ],
  ),
)

Then trigger the form from anywhere:

await Encatch.showForm('your-form-slug');

When showForm is called, the SDK resolves the presenter in this order:

  1. Exact match — first registered EncatchInlineForm whose formId matches the payload wins.
  2. Wildcard — first registered EncatchInlineForm with no formId catches anything not exact-matched.
  3. Modal fallback — EncatchWebView shows the form as the default overlay when no inline slot is registered or none match.

A background tab with EncatchInlineForm mounted will intercept showForm calls even when it is not visible. To prevent this:

Option A — pass enabled from ModalRoute:

EncatchInlineForm(
  formId: 'your-form-slug',
  enabled: ModalRoute.of(context)?.isCurrent ?? true,
)

Option B — only mount EncatchInlineForm on the active route (e.g. using IndexedStack with conditional rendering).

Option C — bottom tabs with GoRouter StatefulShellRoute: Offstage tab branches often do not rebuild when the shell index changes, so ModalRoute.of(context)?.isCurrent can stay stale. Sync the active tab index with an InheritedNotifier (or similar) and pass enabled: activeTabIndex == myTabIndex to each inline slot. The encatch-flutter-tester sample app demonstrates this with ShellTabIndexScope and EncatchInlineForm(enabled: isActive).

When enabled: false the slot is unregistered, so showForm falls through to the modal or another active slot.

The WebView's internal scroll is disabled. The host SingleChildScrollView (or CustomScrollView) provides scrolling. The widget height grows automatically via form:resize messages from the web form.

SingleChildScrollView(
  child: Column(
    children: [
      EncatchInlineForm(formId: 'my-form'),
    ],
  ),
)

The host app controls keyboard avoidance. Wrap the scroll view in MediaQuery inset handling or use Scaffold's resizeToAvoidBottomInset to slide content above the keyboard.

PropTypeDefaultDescription
formIdString?nullExact form slug/id to match. null = wildcard.
enabledbooltrueWhen false, unregisters the slot — use for tab/route focus.
minHeightdouble0Minimum height floor applied after form:resize.
decorationBoxDecoration?nullOuter container decoration.
onOverlayOpenChangeValueChanged<bool>?nullCalled when a QnA/Scheduler overlay opens or closes.

Build Your Own Form UX & UI

If your feedback flow uses a fixed, predictable question set — the same fields and workflow every time — you can build the form with your own Flutter widgets and submit responses through the SDK. That keeps typography, spacing, colors, and interaction patterns aligned with the rest of your app, so the survey feels like a native screen rather than an embedded web page.

Example coming soon.


MethodDescription
init(apiKey, {config})Initialize the SDK
identifyUser(userName, {traits, options})Identify a user
setLocale(locale)Set locale
setCountry(country)Set country (ISO 3166)
setTheme(theme)Set form theme
trackEvent(eventName)Track a custom event
trackScreen(screenName)Track screen navigation
showForm(formId, {options})Show a form (inline or modal)
dismissForm({formConfigurationId})Dismiss the current form
addToResponse(questionId, value)Pre-fill a question answer
startSession({options})Start a new session
pauseSession()Pause background ping
resumeSession()Resume background ping
stopSession()Suspend SDK activity
resetUser()Reset user identity
clearAll()Wipe all persisted SDK data
on(callback)Subscribe to lifecycle events
off(callback)Unsubscribe from events
submitForm(params)Submit a custom native form
stop()Teardown (called by provider on unmount)

Add usage descriptions to ios/Runner/Info.plist when forms include video/audio capture:

<key>NSCameraUsageDescription</key>
<string>Encatch forms use the camera to record video responses.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Encatch forms use the microphone to record audio and video responses.</string>

No other iOS setup is required. The SDK uses flutter_inappwebview, which handles WebView media permission requests once these keys are present.

Add permissions to android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

INTERNET is required for API calls and the form WebView. The other three permissions are required when forms include video/audio capture questions (video_audio).

The SDK grants WebView media permission requests automatically, but Android still requires runtime approval before the camera or microphone can be used. Request them at startup (Android only):

import 'package:flutter/foundation.dart';
import 'package:permission_handler/permission_handler.dart';

Future<void> requestEncatchMediaPermissions() async {
  if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) return;

  final statuses = await [
    Permission.camera,
    Permission.microphone,
  ].request();

  if (statuses[Permission.camera] != PermissionStatus.granted ||
      statuses[Permission.microphone] != PermissionStatus.granted) {
    // Handle denied permissions — recording questions will not work
  }
}

Add permission_handler to your app for this pattern — it is not a dependency of encatch_flutter.

Support

Was this page helpful?