Flutter
Flutter plugin that wires the native Zing SDK on Android and iOS (and provides a Dart-facing API for initialization, logout, and launching native screens).
Current platform support
The Flutter plugin currently supports Android and iOS.
| Platform | Status | Notes |
|---|---|---|
| Android | Fully Supported | All features |
| iOS | Fully Supported | All features |
Requirements
| Requirement | Value / Notes |
|---|---|
| Flutter | 3.3.0+ |
| Android minSdk | 26 (Android 8.0) |
| Android compileSdk | 36+ |
| Dependency injection (Android) | Hilt |
| Maven credentials (Android native SDK) | GitHub Packages token |
| iOS minimum version | 16.0 |
Install the plugin
Add the Git dependency to your pubspec.yaml (the plugin is distributed privately and is not published on pub.dev):
dependencies:
zing_sdk_initializer:
git:
url: https://github.com/Muze-Fitness/fitness-coach-sdk-flutter.git
ref: v2.3.1Check the GitHub releases page for the latest available version.
Then run flutter pub get.
Android setup
To integrate on Android, you must configure your project to support the native Zing SDK dependencies (Hilt + GitHub Packages).
Add Maven repository & credentials
Add the GitHub Packages repository to your Android build (project-level android/build.gradle or settings.gradle):
maven {
url = uri("https://maven.pkg.github.com/Muze-Fitness/fitness-coach-sdk-android")
val localProperties = java.util.Properties()
val localPropertiesFile = File(rootDir, "local.properties")
if (localPropertiesFile.exists()) {
localProperties.load(localPropertiesFile.inputStream())
}
credentials {
username = localProperties.getProperty("zing_sdk_username")
password = localProperties.getProperty("zing_sdk_token")
}
}Ensure your local.properties contains your credentials:
zing_sdk_username=GITHUB_USERNAME
zing_sdk_token=ghp_xxx_with_read_packagesConfigure Hilt (KSP or KAPT)
The native SDK uses Hilt for dependency injection.
- Option A: KSP (recommended)
// Project-level (android/build.gradle or settings.gradle)
id("com.google.dagger.hilt.android") version "2.56.1" apply false
id("com.google.devtools.ksp") version "2.1.20-2.0.0" apply false// App module (android/app/build.gradle)
plugins {
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
}
dependencies {
implementation("com.google.dagger:hilt-android:2.56.1")
ksp("com.google.dagger:hilt-android-compiler:2.56.1")
}- Option B: KAPT (legacy projects)
// Project-level (android/build.gradle or settings.gradle)
id("com.google.dagger.hilt.android") version "2.56.1" apply false// App module (android/app/build.gradle)
plugins {
kotlin("kapt")
id("com.google.dagger.hilt.android")
}
dependencies {
implementation("com.google.dagger:hilt-android:2.56.1")
kapt("com.google.dagger:hilt-android-compiler:2.56.1")
}Custom Application class
Create an Application class extending SdkApplication and annotate it with @HiltAndroidApp:
import coach.zing.fitness.coach.SdkApplication
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApplication : SdkApplication()Register it in AndroidManifest.xml:
<application
android:name=".MyApplication"
... >Update MainActivity
Change MainActivity to inherit from FlutterFragmentActivity and add @AndroidEntryPoint:
import dagger.hilt.android.AndroidEntryPoint
import io.flutter.embedding.android.FlutterFragmentActivity
@AndroidEntryPoint
class MainActivity : FlutterFragmentActivity()Disable the default WorkManager initializer
Add the following provider override inside <application> in AndroidManifest.xml:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>Initialization
Initialize the SDK from Dart during app startup:
import 'package:zing_sdk_initializer/zing_sdk_initializer.dart';
await ZingSdk.instance.init();Android: register a background setup
Android initialization has an extra step: in addition to calling
ZingSdk.instance.init(), register the same setup with registerBackgroundSetup,
passing a top-level function marked @pragma('vm:entry-point'). The SDK uses it to
bring up a headless isolate and initialize itself whenever your main() doesn’t
run — such as restoring an SDK screen’s state or launching the SDK from the
background.
Note:
registerBackgroundSetupis a safe no-op on iOS, so you can call it unconditionally.
import 'package:flutter/widgets.dart';
import 'package:zing_sdk_initializer/zing_sdk_initializer.dart';
/// Used both on app startup and in the headless background isolate.
/// Must be a top-level function annotated with `@pragma('vm:entry-point')`.
@pragma('vm:entry-point')
Future<void> zingSdkSetup() async {
WidgetsFlutterBinding.ensureInitialized();
await ZingSdk.instance.init();
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await zingSdkSetup();
// Android only: lets the SDK run the same setup in a headless isolate.
// No-op on iOS.
await ZingSdk.instance.registerBackgroundSetup(zingSdkSetup);
runApp(const MyApp());
}Initialization errors
| Code | Description |
|---|---|
already_initialized | Called init() more than once |
native_init_failed | Native SDK failed to initialize (internal error) |
Configuration
Pass an SdkConfiguration to customize SDK behavior:
await ZingSdk.instance.init(
configuration: SdkConfiguration(
coachesAvailability: CoachesAvailability.userGenderBased,
genderAvailability: GenderAvailability.binary,
),
);| Field | Type | Default | Description |
|---|---|---|---|
coachesAvailability | CoachesAvailability | allCoaches | allCoaches — all coaches are shown; userGenderBased — only coaches matching the user’s gender. |
genderAvailability | GenderAvailability | all | all — all gender options; binary — male/female only. |
healthBackgroundSync | bool | false | Enables background sync of health data — Health Connect on Android and Apple Health background delivery on iOS. See Health Connect. |
Theme
Pass an SdkTheme to apply branding overrides. Any unset field falls back to SDK defaults.
await ZingSdk.instance.init(
theme: SdkTheme(
colors: SdkColors(
brandPrimary: Color(0xFF1A73E8),
brandSecondary: Color(0xFF34A853),
),
cornersRounding: SdkCornerRounding(
button: SdkRadius.pill(),
),
typography: SdkTypography(
brand: 'YourBrandFont',
system: 'YourSystemFont',
),
),
);| Field | Type | Description |
|---|---|---|
colors | SdkColors | Overrides brand, text, button and background colors. |
cornersRounding | SdkCornerRounding | Overrides corner radius for SDK surfaces. Use SdkRadius.value(n) for a fixed radius or SdkRadius.pill() for a pill shape. |
typography | SdkTypography | Overrides font families for brand and system text. |
blurStyle | SdkBlurStyle | iOS blur style. Android ignores this field. |
To change the theme after initialization, call setTheme. That call replaces the theme and leaves configuration and the current session unchanged. See Working with themes.
Login
After initialization, call login() with an SdkAuthentication value to authenticate the user and start a session.
API Key
Use this mode when backend integration is not available. Pass partnerUserId to link the session to your own user id — if omitted, the SDK generates an id for a new user:
await ZingSdk.instance.login(
SdkAuthentication.apiKey(
ios: 'your-ios-api-key',
android: 'your-android-api-key',
partnerUserId: 'your-user-id',
),
);External Token
Use this mode when your backend manages user authentication and issues JWT tokens. Obtain a token from your backend, then pass it directly to login():
await ZingSdk.instance.login(
SdkAuthentication.externalToken(await yourAuthRepo.getToken()),
);The JWT must contain a sub claim with the partner user ID.
Call login() only on the first session, when the auth state is SdkAuthStateLoggedOut. On subsequent sessions, init() automatically restores the previous session — you do not need to call login() again.
Login errors
| Code | Description |
|---|---|
not_initialized | SDK has not been initialized yet |
login_failed | Login failed (already logged in, login already in progress, or token retrieval failed) |
Logout
Logout and clear the user session.
await ZingSdk.instance.logout();Logout errors
| Code | Description |
|---|---|
not_initialized | SDK has not been initialized yet |
logout_failed | Logout failed (no active session) |
Critical errors
Register a CriticalErrorCallback to be notified when an active session can’t be refreshed (e.g. the refresh token was revoked server-side). Log the user out of your app and call login() again with fresh credentials:
ZingSdk.instance.criticalErrorCallback = _MyCriticalErrorCallback();
class _MyCriticalErrorCallback implements CriticalErrorCallback {
@override
void onCriticalError(PlatformException error) {
// error.code is 'auth_error' or 'unknown'
}
}| Code | Description |
|---|---|
auth_error | The session could not be refreshed and is unrecoverable |
unknown | Unknown critical error |
Auth State
Monitor authentication state changes via the authState stream. userId on SdkAuthStateAuthenticated is the authenticated user’s id — for SdkAuthentication.apiKey this is partnerUserId (or the SDK-generated id, if omitted); for SdkAuthentication.externalToken this is the sub claim of the JWT:
ZingSdk.instance.authState.listen((state) {
switch (state) {
case SdkAuthStateLoggedOut():
// User is not authenticated
case SdkAuthStateInProgress():
// Authentication is in progress
case SdkAuthStateAuthenticated(:final userId):
// User is authenticated
}
});Health Connect (Android)
The SDK integrates with Health Connect to read and write activity data, and since 2.3.1 to read sleep and body measurements. The required Health Connect permissions are declared by the native SDK and merged into your app automatically — you do not need to add them manually:
android.permission.health.READ_STEPS
android.permission.health.READ_HEART_RATE
android.permission.health.READ_EXERCISE
android.permission.health.READ_TOTAL_CALORIES_BURNED
android.permission.health.READ_SLEEP
android.permission.health.READ_WEIGHT
android.permission.health.READ_HEIGHT
android.permission.health.READ_BODY_FAT
android.permission.health.READ_BONE_MASS
android.permission.health.READ_LEAN_BODY_MASS
android.permission.health.READ_BODY_WATER_MASS
android.permission.health.READ_BASAL_METABOLIC_RATE
android.permission.health.WRITE_STEPS
android.permission.health.WRITE_HEART_RATE
android.permission.health.WRITE_EXERCISE
android.permission.health.WRITE_TOTAL_CALORIES_BURNEDDeclare every permission your app requests in the Play Console health apps declaration, including the ones merged from the SDK.
Users who connected Health Connect before 2.3.1 see a one-time prompt on the SDK home screen asking for the sleep and body measurement permissions.
By default, Zing reads and writes Health Connect data while the app is in use, as soon as the user grants the runtime Health Connect permissions. No additional setup is required.
Background sync (optional)
You can additionally enable collecting Health Connect data in the background (e.g. after a reboot, without the app being open). This requires three changes:
1. Add the following permissions to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_HEALTH" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />2. Declare the SDK’s background sync service and boot receiver inside
<application>:
<service
android:name="coach.zing.fitness.coach.service.ZingSdkHCForegroundService"
android:exported="false"
android:foregroundServiceType="health">
<intent-filter>
<action android:name="coach.zing.fitness.sdk.HC_SYNC" />
</intent-filter>
</service>
<receiver
android:name="coach.zing.fitness.coach.broadcast.SdkHealthSyncBootReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>3. Enable it via SdkConfiguration by passing healthBackgroundSync: true:
await ZingSdk.instance.init(
configuration: const SdkConfiguration(
healthBackgroundSync: true,
),
);Background sync is disabled by default (healthBackgroundSync: false).
Theming
Working with themes
Use the theming playground to interactively preview how colors, typography, corner rounding, and images look in the SDK screens before writing any code.
Pass a fully composed SdkTheme to customize all visual aspects of the SDK. Every field is optional — any unset field falls back to the SDK default.
await ZingSdk.instance.init(
theme: SdkTheme(
colors: SdkColors(
brandPrimary: Color(0xFF1A73E8),
brandSecondary: Color(0xFF34A853),
headingPrimary: Color(0xFF1D212C),
headingPrimaryInv: Color(0xFFFFFFFF),
fgPrimary: Color(0xFF1D212C),
fgSecondary: Color(0xFF6B7280),
buttonPrimary: Color(0xFF1A73E8),
buttonSecondary: Color(0xFF34A853),
bgPrimary: Color(0xFFFFFFFF),
bgSecondary: Color(0xFFF3F4F6),
),
blurStyle: SdkBlurStyle.systemMaterial,
cornersRounding: SdkCornerRounding(
button: SdkRadius.pill(),
input: SdkRadius.value(20),
hero: SdkRadius.value(32),
modal: SdkRadius.value(48),
cardSm: SdkRadius.value(8),
cardMd: SdkRadius.value(16),
cardLg: SdkRadius.value(24),
),
typography: SdkTypography(
brand: 'YourBrandFont',
system: 'YourSystemFont',
),
),
);SdkCornerRounding accepts a radius for each SDK surface:
| Field | Used for | Notes |
|---|---|---|
button | Buttons | |
input | Text inputs | iOS only |
hero | Home screen top card | iOS only |
modal | Bottom sheets and modal containers | iOS only |
cardSm | Small cards | iOS only |
cardMd | Medium cards | iOS only |
cardLg | Large cards | iOS only |
All fields are optional — omitting a field keeps the SDK default.
SdkColors fields match the native color tokens. Every field is applied on iOS. Android applies the same fields except brandTertiary, buttonLightYellow, buttonLightOrchid, buttonLightBlue, buttonIconTransparent, overlayBlackDark, overlayBlackMedium, overlayCadetMedium, and the textBody* colors. blurStyle is iOS only.
Call setTheme when the theme should change after init:
await ZingSdk.instance.setTheme(
SdkTheme(
colors: SdkColors(
brandPrimary: Color(0xFF1A73E8),
brandSecondary: Color(0xFF34A853),
),
),
);| Code | Description |
|---|---|
not_initialized | SDK has not been initialized yet |
set_theme_failed | The native SDK rejected the theme update |
Working with pictures
The SDK automatically loads custom images from the host app’s platform resources using fixed asset names. No Flutter-level code is required — just add the images with the correct names to your platform project.
Android
Add drawable files to android/app/src/main/res/drawable/. For best quality, provide density-specific variants — Android will automatically pick the correct one for the device screen:
res/
drawable-mdpi/zing_plan_background.png
drawable-hdpi/zing_plan_background.png
drawable-xhdpi/zing_plan_background.png
drawable-xxhdpi/zing_plan_background.png
drawable-xxxhdpi/zing_plan_background.png| Drawable name | Used for |
|---|---|
zing_plan_background | Plan section background image |
zing_coach_john | Coach John avatar |
zing_coach_jennifer | Coach Jennifer avatar |
zing_coach_sarah | Coach Sarah avatar |
zing_coach_chris | Coach Chris avatar |
iOS
Add image assets to Assets.xcassets using the same names:
| Asset name | Used for |
|---|---|
zing_plan_background | Plan section background image |
zing_coach_john | Coach John avatar |
zing_coach_jennifer | Coach Jennifer avatar |
zing_coach_sarah | Coach Sarah avatar |
zing_coach_chris | Coach Chris avatar |
zing_coach_manel | Coach Manel avatar |
zing_coach_adriana | Coach Adriana avatar |
All image names are optional — the SDK falls back to its built-in assets for any name that is not found.
Working with typography
Pass SdkTypography inside SdkTheme to override the SDK’s built-in fonts. Two font roles are available:
| Field | Applies to | Default |
|---|---|---|
brand | Headings, display text, counter, coach name | SDK built-in (Outfit) |
system | Body text, UI elements, coach chat | SDK built-in (SF Pro / Roboto) |
Both fields are optional — omitting a field keeps the SDK’s built-in font for that role.
The value must be the name the font is registered under in the host app’s platform resources. Register your font first, then pass the same name to SdkTypography:
| Platform | How to register | Name to pass |
|---|---|---|
| Android | Place .ttf/.otf in android/app/src/main/res/font/ | Filename without extension (e.g. "inter") |
| iOS | Add the font file to Xcode and declare it under UIAppFonts in Info.plist | PostScript family name (e.g. "Inter") |
await ZingSdk.instance.init(
theme: SdkTheme(
typography: SdkTypography(
brand: 'MyBrandFont', // registered as res/font/mybrandfont.ttf on Android
system: 'MySystemFont', // registered as PostScript name on iOS
),
),
);User profile
Supply user profile data to personalize the SDK experience. Log in before calling setProfileParams — the call fails with set_profile_params_failed when no user is logged in.
Passing name or gender skips the matching onboarding screens; the other fields pre-fill them. After onboarding, the params update the profile directly.
ProfileParams
All fields are optional. Omit a field to leave the existing value unchanged.
| Field | Type | Notes |
|---|---|---|
name | String? | Display name |
gender | UserGender? | male, female, other; preferNotToSay is iOS only |
height | double? | Centimeters (cm), regardless of measurementSystem |
weight | double? | Kilograms (kg), regardless of measurementSystem |
age | int? | Age in full years |
measurementSystem | MeasurementSystem? | metric or imperial — selects the units shown in the SDK UI only |
Setting the profile
await ZingSdk.instance.setProfileParams(
const ProfileParams(
name: 'Username',
gender: UserGender.female,
height: 178,
weight: 75,
age: 30,
measurementSystem: MeasurementSystem.metric,
),
);Profile errors
| Code | Description |
|---|---|
not_initialized | SDK has not been initialized yet |
set_profile_params_failed | Profile update was rejected (for example, no user is logged in) |
Primary location
Call setPrimaryLocationId after login to store the member’s primary location id from your organisation. The SDK writes it to the user profile and syncs it.
await ZingSdk.instance.setPrimaryLocationId('your-location-id');| Code | Description |
|---|---|
not_initialized | SDK has not been initialized yet |
set_primary_location_id_failed | The id was missing, or no user is logged in |
Launch native screens
await ZingSdk.instance.openScreen(AiAssistantRoute());Supported routes:
HomeRoute()— accepts an optionalHomeScreenConfiguration, see Home screen configurationOnboardingRoute()— runs the onboarding flow; when it completes, the SDK closes and the user returns to your appCustomWorkoutRoute()AiAssistantRoute()WorkoutPlanDetailsRoute()FullScheduleRoute()ProfileSettingsRoute()BodyScanRoute()FlexibilityTestRoute()FitnessTestRoute()
Home screen configuration
HomeRoute accepts a HomeScreenConfiguration that controls which home screen elements are shown. The same configuration is used by ZingHomeView:
await ZingSdk.instance.openScreen(
HomeRoute(
configuration: HomeScreenConfiguration(
showCloseButton: true,
showAskCoachButton: true,
showBodyScanWidget: true,
),
),
);| Parameter | Default | Description |
|---|---|---|
showCloseButton | required | Shows the close button in the home screen toolbar. HomeRoute() without a configuration uses true |
showAskCoachButton | true | Shows the “Ask coach” entry point on the home screen |
showBodyScanWidget | true | Shows the Body Scan card on the home screen. The Fitness Test and Flexibility Test cards stay visible |
Launch screen errors
| Code | Description |
|---|---|
not_initialized | SDK has not been initialized yet |
missing_route | No route argument provided |
unknown_route | Unrecognized route identifier |
no_activity | No activity or root view controller available to present the screen |
not_logged_in | No active user session; call login before opening a screen (iOS only) |
launch_failed | Screen launch failed |
Embedded home screen
Since 2.2.0 the Zing home screen can be embedded directly into your widget tree — as a tab, a page, or any part of your layout — instead of being opened as a full-screen native screen.
ZingHomeView is a single Flutter widget that renders the native home screen on both Android and iOS. On any other platform it renders nothing.
Login required.
ZingHomeViewcan only be shown while the SDK is logged in. Rendering it in any other state throws anIllegalArgumentExceptionon Android. Check theauthStatestream before showing the view.
const ZingHomeView(
configuration: HomeScreenConfiguration(showCloseButton: false),
)The widget takes the same HomeScreenConfiguration as HomeRoute.
Notes
- Configure auth/tokens as described in Authorization before launching UI.