> ## Documentation Index
> Fetch the complete documentation index at: https://developers.learn.ink/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK — Flutter

> Step-by-step guide to integrating the LearnInk Mobile SDK into a Flutter app

<Note>
  **Beta — installed from GitHub.** During the beta you add the plugin as a git dependency
  and build its Android half locally once. When we publish to **pub.dev** and **Maven
  Central**, both steps collapse into a single `flutter pub add learnink_sdk`. See
  [Overview](/sdk/overview).
</Note>

By the end of this guide, tapping a button in your app opens LearnInk with the user
already signed in.

## Prerequisites

* Flutter **3.10 or newer** (`flutter --version`)
* **JDK 17 or newer** for the Android build
* Your app's `minSdkVersion` is **21 or higher**
* A backend endpoint that returns a LearnInk sign-in token —
  see [Step 1 of the WebView setup guide](/integration/webview-setup-guide)
* Your **org ID**
* Access to [github.com/LearnInkTeam/learnink-sdk](https://github.com/LearnInkTeam/learnink-sdk)

<Note>
  The SDK targets **Android**. On other platforms the widget renders a short message in
  place of the LearnInk experience, so a shared codebase still builds and runs.
</Note>

<Steps>
  <Step title="Build the SDK's Android half">
    The Flutter plugin wraps a native Android library, which you build once on your machine:

    ```bash theme={null}
    git clone https://github.com/LearnInkTeam/learnink-sdk.git
    cd learnink-sdk
    git checkout v0.1.0
    cd android
    ./gradlew :sdk:publishToMavenLocal
    ```

    You should see `BUILD SUCCESSFUL`.

    <Warning>
      **Every developer on your team must run this once**, and so must your CI runner. This step
      disappears when we publish to Maven Central.
    </Warning>

    <Accordion title="Build fails with 'Gradle requires JVM 17 or later'">
      Your default JDK is too old:

      ```bash theme={null}
      JAVA_HOME=$(/usr/libexec/java_home -v 21) ./gradlew :sdk:publishToMavenLocal
      ```

      If `flutter build` later fails the same way, point Flutter at a modern JDK once:

      ```bash theme={null}
      flutter config --jdk-dir "$(/usr/libexec/java_home -v 21)"
      ```
    </Accordion>
  </Step>

  <Step title="Add the plugin to your pubspec">
    ```yaml pubspec.yaml theme={null}
    dependencies:
      learnink_sdk:
        git:
          url: https://github.com/LearnInkTeam/learnink-sdk.git
          path: flutter/learnink_sdk
          ref: v0.1.0
    ```

    Then:

    ```bash theme={null}
    flutter pub get
    ```

    <Note>
      `ref: v0.1.0` pins you to a specific release. Without it you track `main`, and your
      teammates may silently get different code.
    </Note>
  </Step>

  <Step title="Let your app find the native library">
    Your app's Android build needs to resolve the library you published in Step 1. In
    **`android/build.gradle`** (or `android/build.gradle.kts`) add `mavenLocal()`:

    ```groovy android/build.gradle theme={null}
    allprojects {
        repositories {
            google()
            mavenCentral()
            mavenLocal() // Beta only — remove once LearnInk publishes to Maven Central
        }
    }
    ```

    <Note>
      Newer Flutter templates may not have an `allprojects` block. If yours doesn't, add one —
      it goes at the top level of `android/build.gradle`.
    </Note>
  </Step>

  <Step title="Write your token function">
    Create a function that calls **your** backend and returns the token string. This example
    uses the `http` package:

    ```dart theme={null}
    import 'dart:convert';
    import 'package:http/http.dart' as http;

    Future<String> fetchLearnInkToken() async {
      final response = await http.post(
        Uri.parse('https://your-api.example.com/auth/learnink'),
        headers: {
          'Content-Type': 'application/json',
          // Authenticate this call however your own API expects:
          'Authorization': 'Bearer $yourOwnUserSessionToken',
        },
        body: jsonEncode({'id': currentUserId}),
      );

      if (response.statusCode != 200) {
        throw Exception('Token request failed: HTTP ${response.statusCode}');
      }
      return jsonDecode(response.body)['token'] as String;
    }
    ```

    <Warning>
      **Never call the LearnInk Identify API directly from your app.** That would put your API
      key on user devices, where it can be extracted. Your app calls *your* backend; your
      backend holds the key.
    </Warning>
  </Step>

  <Step title="Show the LearnInk widget">
    `LearnInkWebView` is an ordinary widget — put it wherever you like:

    ```dart theme={null}
    import 'package:flutter/material.dart';
    import 'package:learnink_sdk/learnink_sdk.dart';

    class LearnInkScreen extends StatelessWidget {
      const LearnInkScreen({super.key});

      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: SafeArea(
            child: LearnInkWebView(
              orgId: 'your-org-id',
              path: 'learning',
              fetchToken: fetchLearnInkToken,
              onClose: () => Navigator.of(context).pop(),
            ),
          ),
        );
      }
    }
    ```

    Navigate to it however you normally would:

    ```dart theme={null}
    Navigator.of(context).push(
      MaterialPageRoute(builder: (_) => const LearnInkScreen()),
    );
    ```

    <Note>
      The widget fills whatever space its parent gives it. Inside a `Column` or `Row`, wrap it in
      `Expanded`, or it will have zero height and render nothing.
    </Note>
  </Step>

  <Step title="Verify it works">
    Run on an Android device or emulator. You should see:

    * ✅ A brief loading indicator, then LearnInk content
    * ✅ **No login screen** — the user is signed in automatically
    * ✅ Tapping close inside LearnInk pops the route

    If you see a **login screen**, the token did not reach LearnInk — see
    [Troubleshooting](/sdk/troubleshooting).
  </Step>
</Steps>

## Controlling the view

Capture a controller to drive the widget after it is built:

```dart theme={null}
LearnInkWebViewController? _controller;

LearnInkWebView(
  // …
  onWebViewCreated: (controller) => _controller = controller,
)

// Later:
await _controller?.refreshSession(); // Re-authenticate and reload
await _controller?.goBack();         // Back through WebView history
final canGoBack = await _controller?.canGoBack() ?? false;
```

## Widget reference

| Property                      | Default                | What it does                                                              |
| ----------------------------- | ---------------------- | ------------------------------------------------------------------------- |
| `orgId`                       | *required*             | Your LearnInk organisation ID                                             |
| `path`                        | *required*             | Section to open, e.g. `'learning'`                                        |
| `fetchToken`                  | *required*             | `Future<String> Function()` — calls your backend                          |
| `baseUrl`                     | `https://m.learn.ink`  | Only change if given a staging URL                                        |
| `tokenTimeout`                | `Duration(seconds: 5)` | How long to wait for `fetchToken`                                         |
| `openExternalLinksInBrowser`  | `true`                 | External links open in the device browser                                 |
| `extraHttpHeaders`            | `{}`                   | Extra headers on page loads (rarely needed)                               |
| `offlineSupportEnabled`       | `true`                 | Show downloaded content when offline                                      |
| `offlineFirstDelay`           | `Duration(seconds: 1)` | Show downloaded content if the token is slower than this                  |
| `onWebViewCreated`            | —                      | Gives you the controller                                                  |
| `onClose`                     | —                      | User tapped close inside LearnInk                                         |
| `onAuthenticationFailed`      | —                      | Token fetch failed; page loaded signed-out                                |
| `onSessionRefreshFailed`      | —                      | Re-authentication failed; offer a retry                                   |
| `onOfflineModeEntered`        | —                      | Offline; showing downloaded content                                       |
| `onOfflineContentUnavailable` | —                      | Offline **and** nothing downloaded — [show your own screen](/sdk/offline) |
| `onConnectivityRestored`      | —                      | Network came back                                                         |
| `onMessage`                   | —                      | A message the SDK does not handle itself                                  |

## Next steps

<CardGroup cols={2}>
  <Card title="Offline behaviour" icon="cloud-arrow-down" href="/sdk/offline">
    What your app must show when a user has no connection and nothing downloaded.
  </Card>

  <Card title="Push notifications" icon="bell" href="/integration/push-notifications">
    Still required — the SDK does not change how notifications reach users.
  </Card>
</CardGroup>
