Android setup

Add the Folio SDK to an Android app, initialize the platform, create the runtime, mount stores and run an inquiry with the Compose inquiry UI.

Preview

This page describes a pre-release version of the SDK. Names, versions and APIs on this page can change before the release.

This page adds the Folio SDK to an Android app. It covers the Gradle setup, the manifest, platform initialization, the runtime, stores and the inquiry UI. For the concepts behind the runtime and the stores, read Runtime first.

Requirements

ItemValue
minSdk23 or higher
compileSdkThe SDK is compiled against API level 34
JavaSource and target compatibility 17, Kotlin jvmTarget 17
Android Gradle PluginThe SDK is built with 8.13.0
KotlinThe SDK is built with 2.2.10
UI toolkitJetpack Compose (the inquiry UI and rememberFolioSdk)
ABIsarm64-v8a, armeabi-v7a, x86_64

Artifacts

ArtifactContents
id.folio:sdkThe Kotlin API in id.folio.sdk: FolioSdk, rememberFolioSdk, FolioSDKProvider, the stores (SessionStore, AuthStore, VaultStore, InquiryStore, MrtdStore, LogsStore, FolioDocumentListStore, FolioDocumentStore) with their actions, state types and remember composables. The inquiry UI in id.folio.sdk.inquiry. The platform layer XPlatform in id.folio.sdk.platform. The icon images of FolioSdk.iconAsset and their resolver FolioResourceIcons in id.folio.sdk.resources; see Static functions. DefaultSelfieCaptureProvider in id.folio.sdk.inquiry.ui.verification.selfie, the selfie capture provider of the inquiry UI, built on the capture engine of the selfie step
id.folio:sdk-nativeThe native library of the SDK for the ABIs above

An app that runs the SDK adds id.folio:sdk only. Its POM depends on id.folio:sdk-native of the same release and on the capture engine of the selfie step, so Gradle pulls both in (see Dependencies).

id.folio:sdk-native holds two shared libraries per ABI: libid_folio.so with the SDK core and libid_folio_jni.so with the JNI bridge, which is linked for 16 KB memory pages. Its sources jar carries the C sources of the JNI bridge. id.folio:sdk depends on id.folio:sdk-native of its own version; do not force another version of it. When the Kotlin API first loads the native libraries, it compares their bindgen format version and metadata digest with its own. When they differ, the first SDK call throws an ExceptionInInitializerError whose cause is an IllegalStateException that names both values, and every later call throws NoClassDefFoundError.

The sha256 of libid_folio.so in the provenance file and in FolioSDKBuildInfo.NATIVE is the digest of the file in the Maven repository, jni/<abi>/libid_folio.so inside the id.folio:sdk-native AAR. When the Android Gradle plugin packages your APK, it strips the library and rewrites its ELF section tables, so the copy inside the APK has another digest, in debug and release builds alike. To keep the published bytes in your APK, exclude the library from stripping; otherwise compare the AAR, not the APK:

build.gradle.kts
android {
    packaging {
        jniLibs {
            keepDebugSymbols += "**/libid_folio.so"
        }
    }
}

Install

Repositories

Folio gives you access to its Maven repository. Add it to your Gradle settings next to Google and Maven Central. The Folio repository also serves the capture engine of the selfie step, so no other repository is needed:

settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url = uri("<android-maven-repository-url>") }
    }
}

Dependencies

app/build.gradle.kts
dependencies {
    implementation("id.folio:sdk:<version>")
}

<version> is the SDK release version Folio gives you.

id.folio:sdk pulls in id.folio:sdk-native of the same version and the capture engine of the selfie step at fixed versions, which the Folio Maven repository serves.

id.folio:sdk exposes as API dependencies the libraries its public types use, so your app compiles against them without declaring them: androidx.activity:activity-compose 1.9.3, androidx.lifecycle:lifecycle-viewmodel 2.7.0, androidx.compose.runtime:runtime, androidx.compose.foundation:foundation, androidx.compose.ui:ui and androidx.compose.ui:ui-text 1.7.5, org.jetbrains.kotlinx:kotlinx-coroutines-android 1.7.3 and org.jetbrains.kotlinx:kotlinx-serialization-json 1.7.3. These are minimum versions: Gradle resolves each of them to the newer version your app declares. The identity record mapper you write (see Create the runtime) encodes your own JSON document; the samples on this page use kotlinx.serialization for it, which needs the Kotlin serialization plugin in your app module.

Compose and Kotlin plugins

rememberFolioSdk, FolioSDKProvider and FolioInquiry.Host are composables. Enable Compose with the Kotlin Compose compiler plugin and set Java 17:

app/build.gradle.kts
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("org.jetbrains.kotlin.plugin.serialization")
    id("org.jetbrains.kotlin.plugin.compose")
}

android {
    compileSdk = 34

    defaultConfig {
        minSdk = 23
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    buildFeatures {
        compose = true
    }
}

kotlin {
    compilerOptions {
        jvmTarget = JvmTarget.JVM_17
    }
}

JvmTarget is org.jetbrains.kotlin.gradle.dsl.JvmTarget.

A release published again

Folio can publish a release again under the same version. Gradle caches the artifacts of a version it has downloaded and keeps using them. After a release is published again, run your build once with --refresh-dependencies, so that Gradle checks the cached artifacts against the repository and downloads the ones that changed:

./gradlew assembleDebug --refresh-dependencies

When <android-maven-repository-url> is a file URL of a local clone of the Maven repository, Gradle reads the artifacts from the clone and caches nothing from it, so --refresh-dependencies changes nothing. Update the clone instead, then build again:

git fetch --tags --force
git checkout v<version>

To confirm the build your app runs, read the revision of the Stamped product of FolioSdk.sdkBuildInfo(), or compare FolioSDKBuildInfo.NATIVE with the provenance file provenance/<version>.json of the release.

Manifest

What the SDK merges

The manifest of id.folio:sdk merges into your app's manifest. It declares:

EntryNotes
android.permission.INTERNET
android.permission.ACCESS_NETWORK_STATE
android.permission.POST_NOTIFICATIONS
android.permission.NFCChip reading of passports and ID cards
android.permission.CAMERADocument and selfie capture in the inquiry UI
uses-feature android.hardware.nfcrequired="false"
uses-feature android.hardware.camerarequired="false"
uses-feature android.hardware.camera.anyrequired="false"
receiver id.folio.sdk.platform.XPlatformNotificationReceiverexported="false"

id.folio:sdk-native merges no entries.

The inquiry UI asks the user for the camera permission at runtime when it opens the camera. When the user denies it, the camera screen shows its denied state.

POST_NOTIFICATIONS and the receiver serve the notification service of the SDK's platform layer, which products built on the SDK, such as Folio's own apps, use. None of the stores on this page posts a notification, so with the SDK alone the receiver never fires and no launch intent carries the SDK's folioTarget extra.

What the dependencies of the SDK merge

The libraries id.folio:sdk depends on merge their own manifest entries into your app. Among others:

DependencyEntries
androidx.work:work-runtime 2.9.0WAKE_LOCK, RECEIVE_BOOT_COMPLETED, FOREGROUND_SERVICE and ACCESS_NETWORK_STATE; the receiver androidx.work.impl.background.systemalarm.RescheduleReceiver on BOOT_COMPLETED, TIME_SET and TIMEZONE_CHANGED, and the other receivers, services and the provider of WorkManager; through Room, which WorkManager depends on, the service androidx.room.MultiInstanceInvalidationService
androidx.biometric:biometric 1.1.0USE_BIOMETRIC and USE_FINGERPRINT
The capture engine of the selfie stepVIBRATE, CAMERA and FLASHLIGHT; uses-feature android.hardware.camera.autofocus with required="false"; the activities of the engine and a Bluetooth service of the engine; android:largeHeap="true" on the application
androidx.camera:camera-camera2 1.3.4The disabled service androidx.camera.core.impl.MetadataHolderService with its default configuration provider
androidx.credentials and Google Play services sign-inThe activities and the services of the credential provider and of Google sign-in, and the meta-data com.google.android.gms.version
androidx.coreThe permission <applicationId>.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION
androidx.startup and androidx.profileinstallerThe provider androidx.startup.InitializationProvider with the initializers of WorkManager, of androidx.emoji2 and androidx.lifecycle:lifecycle-process (through Compose UI), of OkHttp and of androidx.profileinstaller, and the receiver androidx.profileinstaller.ProfileInstallReceiver

RescheduleReceiver reschedules WorkManager work after a reboot and after a change of the time or the time zone. It does not restore the SDK's notifications.

The SDK and its dependencies merge every permission the SDK uses, so your app adds none. WAKE_LOCK, RECEIVE_BOOT_COMPLETED and FOREGROUND_SERVICE arrive through WorkManager, USE_BIOMETRIC and USE_FINGERPRINT through androidx.biometric, and VIBRATE through the capture engine of the selfie step.

The platform layer schedules a notification as an inexact alarm (setAndAllowWhileIdle), which needs no exact-alarm permission; the system delivers it with a delay it chooses. The SDK registers no receiver of its own for BOOT_COMPLETED, so a scheduled notification that is still pending at a reboot is lost.

Initialize the platform

Call XPlatform.init once per process, before you create a runtime. The right place is the onCreate of your Application subclass:

ExampleApplication.kt
class ExampleApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        XPlatform.init(this)
    }
}

XPlatform is in the id.folio.sdk.platform package, imported as import id.folio.sdk.platform.XPlatform. The rules:

  • init takes any Context and keeps its application context.
  • init loads the SDK's native library and hands the application context to it. Never call System.loadLibrary for the SDK yourself.
  • A second call throws IllegalStateException with the message "XPlatform already initialized". An activity can be created more than once in a process, so do not call init from an activity. XPlatform.isInitialized tells you whether init has run.
  • init registers activity lifecycle callbacks that track the resumed activity. NFC reading, biometric prompts and notifications use that activity.
  • Creating a runtime before init fails.

Create the runtime

Configuration

The runtime starts from a FolioSdkConfig:

private val folioConfig = FolioSdkConfig(
    baseUrl = "https://app.folio.mobi",
    organization = "<organization>",
    logging = FolioSdk.loggingConfig(null),
)
FieldTypeValue
baseUrlStringThe Folio origin, https://app.folio.mobi
organizationStringThe organization Folio gives you. The SDK builds your vault record store for it
developmentDevelopmentConfig?Defaults to null. A realm and service environments; see Configuration.
loggingLoggingConfigFile and console writers for this runtime; see Logging.

The SDK reads your app's identity from the package: the package name as the identifier, versionName as the version and longVersionCode as the build (versionCode below API 28). In a stamped build, create fails when versionName is missing or empty (see Application identity). The SDK reports the version and build in its user agent.

All types are in id.folio.sdk. See Configuration for every field in detail.

Identity record mapper

The runtime takes an IdentityRecordMapper next to the configuration and keeps it for its lifetime. When a government ID verification succeeds, the inquiry calls map(identity, existing) and writes the RecordContent you return to the vault. payload is your own JSON document; schema is your name for it. existing is the record the SDK saved earlier for the same physical document, or null for a new one. An exception thrown from map writes nothing and ends the inquiry with InquiryError.RecordMapping. See Identity records for the full contract.

SavedIdentityMapper.kt
@Serializable
data class SavedIdentity(
    val identities: List<VerifiedIdentity>,
)

object SavedIdentityMapper : IdentityRecordMapper {
    const val SCHEMA = "identity"
    const val VERSION = 1L

    override fun map(identity: VerifiedIdentity, existing: RecordContent?): RecordContent {
        val earlier = existing?.let { record ->
            require(record.schema == SCHEMA && record.version == VERSION) {
                "a ${record.schema} v${record.version} record is no saved identity"
            }
            Json.decodeFromString<SavedIdentity>(record.payload).identities
        }.orEmpty()
        return RecordContent(
            schema = SCHEMA,
            version = VERSION,
            payload = Json.encodeToString(SavedIdentity(identities = earlier + identity)),
        )
    }
}

rememberFolioSdk

rememberFolioSdk(config, mapper) creates a FolioSdk and returns its startup state as a FolioSdkStartup.

StateMeaning
FolioSdkStartup.StartingThe runtime is being created
FolioSdkStartup.FailedCreation failed; error is the Throwable that creation threw, retry() creates the runtime again
FolioSdkStartup.ReadyThe runtime is ready; runtime is the FolioSdk

The runtime lives in a FolioSdkHolder, a ViewModel of the nearest ViewModelStoreOwner, such as your activity:

  • Creation runs in the view model's scope. A configuration change that recreates the activity keeps the same runtime.
  • When the owner is destroyed for good, the holder cancels a creation in progress and closes the runtime. Do not close a runtime from rememberFolioSdk yourself.
  • A call with a different config (compared with equals, FolioSdkConfig is a data class) or a different mapper instance (compared by identity) cancels the running creation, closes the current runtime and creates a new one; the call returns Starting until it is ready. Keep the mapper one instance, such as a Kotlin object, so that a recreated activity does not restart the runtime.
  • retry() creates the runtime again with the same config and mapper. It does nothing when the Failed value is no longer the current state.
setContent {
    MaterialTheme {
        when (val startup = rememberFolioSdk(folioConfig, SavedIdentityMapper)) {
            FolioSdkStartup.Starting -> Unit
            is FolioSdkStartup.Failed -> {
                LaunchedEffect(startup) {
                    Log.e(TAG, "runtime creation failed", startup.error)
                }
            }
            is FolioSdkStartup.Ready -> Verification(startup.runtime, launchCode)
        }
    }
}

Without Compose

FolioSdk.create(config, mapper) is a suspend function that returns the FolioSdk or throws when creation fails. Cancelling the calling coroutine cancels the creation. A runtime you create this way is yours to close. close() and shutdown() are suspend functions. close() shuts the runtime down once and releases it, even when the shutdown throws; it runs to the end when the calling coroutine is cancelled. A second close() returns the result of the first one; any other call on a closed runtime throws IllegalStateException. shutdown() unmounts every store and shuts the session scope down without releasing the handle; close() calls it for you.

lifecycleScope.launch {
    val runtime = FolioSdk.create(folioConfig, SavedIdentityMapper)
}

Static functions

The runtime does not return data from its methods. Everything goes through stores. Functions that need no runtime are @JvmStatic members of the companion object of FolioSdk, such as FolioSdk.localize(key, locale), FolioSdk.availableLocales(), FolioSdk.countries() and FolioSdk.sdkBuildInfo(). See Static functions for the full list and Localization.

Stores

A store exists only while it is mounted on the runtime. Every Android store has the same shape:

MemberBehaviour
Store.mount(runtime)Mounts the store and reads its initial state synchronously. Throws FolioSDKException on failure, IllegalStateException when the runtime is closed
stateA StateFlow of the store's UI model. The value is never null
dispatch(action, context)Sends one action. context is an optional LogContext, null by default, that ties the action's journal entries to an interaction; see Logging. Throws FolioSDKException synchronously when the call fails
streamErrorA StateFlow<Throwable?>. It holds the error when the stream that keeps state current fails, and null otherwise
subscribe()Returns an AutoCloseable subscription, such as VaultSubscription. Its suspend next() returns the current UI model first, then the latest one after each change, and null once the store is unmounted; asFlow() exposes the same values as a Flow
close()Unmounts the store and cancels its running work. A second call does nothing. state keeps its last value; dispatch and subscribe after close() throw IllegalStateException

mount takes the host interface of its store, such as VaultHost for VaultStore; FolioSdk implements all of them.

StoreStateActionHostComposablePage
SessionStoreSessionUiModelSessionActionSessionHostrememberSessionStore()Session
AuthStoreAuthUiModelAuthActionAuthHostrememberAuthStore()Authentication
VaultStoreVaultUiModelVaultActionVaultHostrememberVaultStore()Vault
InquiryStoreInquiryUiModelInquiryActionInquiryHostrememberInquiryStore()Inquiry store
MrtdStoreMrtdUiModelMrtdActionMrtdHostrememberMrtdStore()NFC
LogsStoreLogsUiModelLogsActionDiagnosticsHostrememberLogsStore(init)Logging
FolioDocumentListStoreFolioDocumentListUiModelFolioDocumentListActionFolioDocumentListHostrememberFolioDocumentListStore()Folio documents
FolioDocumentStoreFolioDocumentUiModelFolioDocumentActionFolioDocumentHostrememberFolioDocumentStore(init)Folio documents

FolioSDKException carries a numeric code and a message; its companion object names the codes, such as FolioSDKException.INVALID_ARGUMENT.

The result of an action lands in the store's state, never in a return value. Closing a store cancels what it is doing, so keep a store mounted until the state shows the result you wait for.

In Compose, mount a store with remember, close it with DisposableEffect and collect its state with collectAsState:

@Composable
fun SavedIdentities(runtime: FolioSdk) {
    val vault = remember(runtime) { VaultStore.mount(runtime) }
    DisposableEffect(vault) {
        onDispose { vault.close() }
    }
    val model by vault.state.collectAsState()
    val identities = model.records
        .filter { it.header.schema == SavedIdentityMapper.SCHEMA }
        .map { Json.decodeFromString<SavedIdentity>(it.payload) }
}

FolioSDKProvider

FolioSDKProvider(runtime) { ... } puts a runtime into the composition. Inside it, LocalFolioSdk.current is the FolioSdk, and every host local, such as LocalVaultHost, holds the same runtime. Each remember<Name>Store() composable from the table above reads its host local, mounts its store on that host with remember, and closes it with DisposableEffect when it leaves the composition, as the sample above does by hand. Reading LocalFolioSdk or a host local, or calling a store composable, outside a provider throws IllegalStateException.

setContent {
    MaterialTheme {
        when (val startup = rememberFolioSdk(folioConfig, SavedIdentityMapper)) {
            FolioSdkStartup.Starting -> Unit
            is FolioSdkStartup.Failed -> Button(onClick = startup::retry) { Text("Retry") }
            is FolioSdkStartup.Ready -> FolioSDKProvider(startup.runtime) {
                SavedIdentities()
            }
        }
    }
}

@Composable
fun SavedIdentities() {
    val vault = rememberVaultStore()
    val model by vault.state.collectAsState()
    val identities = model.records
        .filter { it.header.schema == SavedIdentityMapper.SCHEMA }
        .map { Json.decodeFromString<SavedIdentity>(it.payload) }
}

FolioSDKProvider provides every host interface of the runtime. To provide one host only, use SessionHostProvider, AuthHostProvider, VaultHostProvider, InquiryHostProvider, MrtdHostProvider, DiagnosticsHostProvider, FolioDocumentListHostProvider or FolioDocumentHostProvider, which take the host and content the same way and set LocalSessionHost, LocalAuthHost, LocalVaultHost, LocalInquiryHost, LocalMrtdHost, LocalDiagnosticsHost, LocalFolioDocumentListHost or LocalFolioDocumentHost. The matching remember<Name>Store() works below such a provider.

rememberInquiryStore() mounts an InquiryStore without opening an inquiry; to run one, use FolioInquiry.open (see Run an inquiry).

Run an inquiry

Your backend creates the inquiry and passes its launch code to your app. The inquiry UI is in id.folio.sdk.inquiry.FolioInquiry. For the flow itself, see Inquiry UI.

Open the store

FolioInquiry.open(runtime, entry) takes an InquiryHost, such as your FolioSdk, mounts an InquiryStore and dispatches InquiryAction.Open(entry). If the dispatch throws, open closes the store and rethrows.

EntryUse
InquiryEntry.Start(launchCode)Start from the single-use launch code your backend received
InquiryEntry.Resume(ResumeSource.SameDevice(inquiryId))Resume an inquiry on this device; inquiryId is nullable
InquiryEntry.Resume(ResumeSource.AnotherDevice(code))Continue an inquiry handed off from another device

Host the flow in Compose

FolioInquiry.Host(store, platform) renders the inquiry flow as a composable. platform is an InquiryPlatform:

class InquiryPlatform(
    val nfc: MrtdHost,
    val selfieCapture: SelfieCaptureProvider = DefaultSelfieCaptureProvider(),
)

nfc is the runtime that reads the chip, your FolioSdk. selfieCapture is the selfie engine, DefaultSelfieCaptureProvider() unless you pass your own.

@Composable
private fun Verification(runtime: FolioSdk, launchCode: String) {
    val store = remember(runtime, launchCode) {
        FolioInquiry.open(runtime, InquiryEntry.Start(launchCode))
    }
    DisposableEffect(store) {
        onDispose { store.close() }
    }
    val platform = remember(runtime) {
        InquiryPlatform(nfc = runtime)
    }
    val model by store.state.collectAsState()
    LaunchedEffect(model.lifecycle) {
        when (val lifecycle = model.lifecycle) {
            is InquiryLifecycle.Ready -> Log.i(TAG, "inquiry ready: sdk ${lifecycle.sdkVersion}")
            is InquiryLifecycle.Started -> Log.i(TAG, "inquiry started: ${lifecycle.correlationId}")
            is InquiryLifecycle.Closed -> {
                Log.i(TAG, "inquiry closed: ${lifecycle.outcome}")
                finish()
            }
            is InquiryLifecycle.Failed -> Log.e(TAG, "inquiry failed: ${lifecycle.error}")
        }
    }
    FolioInquiry.Host(store, platform)
}

Host does not close the store or finish your activity: react to InquiryLifecycle.Closed yourself and close the store then. Closed arrives only after the flow dispatched InquiryAction.Close; Failed is informational, the flow still shows the failure notice and the person leaves through its close button.

System Back

Host and present handle the system Back gesture and button themselves; add no BackHandler of your own around them. Back does what the screen allows:

On screenBack
An open sheet, such as an option list or a dialogCloses the sheet.
A step that can go back (navigation.canGoBack)Goes back one step, as the back button of the step does.
A step with a top bar but no way backAsks whether to leave the inquiry, as the bar's close button does; leaving dispatches Close.
The terminal stage, the loading state or a failure noticeDispatches Close.

After the flow dispatched Close it sends nothing more to the store until you dispatch the next Open to the same store, and it handles no Back while lifecycle is Closed.

Present in an activity

FolioInquiry.present(activity, store, platform) takes a ComponentActivity and:

  1. enables edge-to-edge with transparent status and navigation bars;
  2. calls setContent on that activity with the inquiry flow inside a MaterialTheme and a full-size Surface;
  3. finishes the activity when lifecycle becomes InquiryLifecycle.Closed, unless it is already finishing.
FolioInquiry.present(activity = this, store = store, platform = platform)

present replaces the content of the activity instance you pass. It does not start a new activity, and it does not close the store.

Lifecycle and state

The inquiry UI dispatches InquiryAction.Close when the user leaves the flow: from the terminal stage after the redirect, from a close button or the failure notice, or on Back as described above. lifecycle becomes Closed only after that Close; while the terminal stage shows it stays Started. Watch InquiryUiModel.lifecycle:

LifecycleField
InquiryLifecycle.ReadysdkVersion
InquiryLifecycle.StartedcorrelationId
InquiryLifecycle.Closedoutcome, a TerminalOutcome or null
InquiryLifecycle.Failederror, an InquiryError

InquiryUiModel also carries view (the current screen), loading, busy, chrome (the localized texts of the flow), error (an InquiryErrorView with text, placement and retryable), failure (the failure notice of an inquiry that cannot go on, which the UI shows instead of any step) and identity (the VerifiedIdentity of a successful government ID verification). See Inquiry store and Errors.

The SDK validates and normalizes every link URL of a step and the redirect URL of a finished inquiry: HTTPS://Example.COM becomes https://example.com/, and a space in a query becomes %20. A URL that does not parse fails with InquiryError.InvalidLink, whose url is the rejected value.

The inquiry UI opens every link, a LinkTarget.External, LinkTarget.Universal or LinkTarget.Webview alike, with an Intent.ACTION_VIEW intent for its URL. Android resolves the intent as it resolves any link: an app that handles the URL, such as your own app through Android App Links, or otherwise the browser. When no app can open the URL, the UI sends InquiryIntent.LinkRefused(url) and shows the error the store projects, a banner with the localized error text.

When the inquiry ends, the terminal stage carries an optional redirect in StageContent.Terminal.redirect, a LinkTarget.Universal. Once loading is false, the inquiry UI opens the redirect and then dispatches InquiryAction.Close, so present finishes the activity after the redirect opens. Without a redirect it dispatches Close at once. When no app can open the redirect, the UI sends LinkRefused instead and shows the error; its close button dispatches Close.

Activity recreation

The store and the flow are not saved in the activity's instance state:

  • The runtime from rememberFolioSdk lives in a ViewModel of the ViewModelStoreOwner and survives the recreation, as long as config stays equal and mapper stays the same instance.
  • A store kept with remember lives as long as the composition that holds it.
  • present sets the content of one activity instance; a recreated instance shows nothing from the inquiry until you call present again.

To keep rotation from recreating the activity that hosts the inquiry, declare android:configChanges="orientation|screenSize|keyboardHidden" on that activity.

Selfie engine

The inquiry UI captures selfies through the SelfieCaptureProvider of the InquiryPlatform you pass to Host or present. id.folio:sdk ships DefaultSelfieCaptureProvider (id.folio.sdk.inquiry.ui.verification.selfie), which the InquiryPlatform uses by default:

val platform = InquiryPlatform(nfc = runtime)

While the step is being checked, or when the step has no liveness service URL, the selfie step shows its processing view and captures nothing.

DefaultSelfieCaptureProvider runs the capture engine of the selfie step in server-side mode: it carries no license of its own and receives the service address and request headers from the SDK for each capture. It runs passive or active liveness as the inquiry asks, skips the engine's onboarding and success screens, shows a close button, does not offer a camera switch and hides the engine's copyright line. It returns the captured image as JPEG.

Custom engine

To use another liveness engine, implement the interface from id.folio.sdk.inquiry.ui.verification.selfie:

interface SelfieCaptureProvider {
    suspend fun capture(context: Context, capture: SelfieCaptureView): SelfieCaptureResult
}
SelfieCaptureView fieldTypeMeaning
captureSessionIdStringThe capture session the liveness check belongs to
modeSelfieLivenessModePASSIVE or ACTIVE
serviceUrlString?The liveness service address
headersList<HeaderView>Request headers, each with name and value
textsSelfieCaptureTextsThe localized texts the engine shows

SelfieCaptureTexts has the optional texts holdSteady, turnHead, lookStraight, centerFace and processing. The UI calls the provider only with a serviceUrl.

Return a SelfieCaptureResult(image, transactionId): image is the captured image as a ByteArray, transactionId the liveness transaction id. The inquiry submits the transaction id and moves on. Throw an exception to give up: the inquiry goes back one step. A cancelled coroutine is passed through as a cancellation.

NFC

Passport and ID card chip reading runs inside the inquiry flow through MrtdStore. It needs the device's NFC adapter to be on, and it reads through the activity that is resumed at that moment. See NFC.

Logging

The console writer of FolioSdkConfig.logging writes to logcat with the tag folio:

  • LogWriterConfig.Console selects the lowest console level written: TRACE, DEBUG, INFO, WARN or ERROR, as LogLevel values. They map to Log.v, Log.d, Log.i, Log.w and Log.e.
  • An entry longer than 3000 characters arrives as several logcat entries prefixed [0], [1] and so on. Join them in index order.
  • FolioSdk.loggingConfig(consoleLevel) adds a console writer at that level to the file writer of the log journal; with null the SDK writes the journal only and nothing reaches logcat. Each runtime configures its own writers.

See Logging.

Shrinking

id.folio:sdk ships consumer ProGuard rules that R8 applies to your app automatically:

consumer-rules.pro
-keep class id.folio.sdk.** { *; }

You add no rules for the SDK yourself.

Errors

  • FolioSDKException is thrown by mount and dispatch of a store.
  • FolioSdkStartup.Failed.error carries the runtime creation failure as a Throwable; FolioSdk.create throws it.
  • Inquiry failures arrive as InquiryLifecycle.Failed with an InquiryError, as InquiryUiModel.error for errors a step shows, and as InquiryUiModel.failure for an inquiry that cannot go on. An inquiry whose selfie photo step has no capture field fails with InquiryError.MissingCaptureField, whose step is the step id.

See Errors.

Minimal app

A minimal host of the inquiry: the application initializes the platform, the activity creates the runtime and shows the inquiry for one launch code with the default selfie engine.

MainActivity.kt
class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val config = FolioSdkConfig(
            baseUrl = "https://app.folio.mobi",
            organization = ORGANIZATION,
            logging = FolioSdk.loggingConfig(null),
        )

        setContent {
            MaterialTheme {
                when (val startup = rememberFolioSdk(config, SavedIdentityMapper)) {
                    FolioSdkStartup.Starting -> Unit
                    is FolioSdkStartup.Failed -> {
                        LaunchedEffect(startup) {
                            Log.e(TAG, "runtime creation failed", startup.error)
                        }
                    }
                    is FolioSdkStartup.Ready -> Inquiry(startup.runtime)
                }
            }
        }
    }

    @Composable
    private fun Inquiry(runtime: FolioSdk) {
        val store = remember(runtime) {
            FolioInquiry.open(runtime, InquiryEntry.Start(LAUNCH_CODE))
        }
        DisposableEffect(store) {
            onDispose { store.close() }
        }
        val model by store.state.collectAsState()
        LaunchedEffect(model.lifecycle) {
            if (model.lifecycle is InquiryLifecycle.Closed) finish()
        }
        val platform = remember(runtime) {
            InquiryPlatform(nfc = runtime)
        }
        FolioInquiry.Host(store, platform)
    }

    private companion object {
        const val TAG = "InquiryExample"
        const val ORGANIZATION = "<organization>"
        const val LAUNCH_CODE = "paste-a-launch-code-from-your-backend-here"
    }
}

The SDK imports this sample uses:

import id.folio.sdk.FolioSdk
import id.folio.sdk.FolioSdkConfig
import id.folio.sdk.FolioSdkStartup
import id.folio.sdk.InquiryEntry
import id.folio.sdk.InquiryLifecycle
import id.folio.sdk.inquiry.FolioInquiry
import id.folio.sdk.inquiry.InquiryPlatform
import id.folio.sdk.rememberFolioSdk

Known limits

  • XPlatform.init runs once per process; a second call throws.
  • Biometric prompts and passkey requests need a resumed FragmentActivity. They fail when the resumed activity is a plain ComponentActivity.
  • The native library covers arm64-v8a, armeabi-v7a and x86_64 only.
  • An internal fault inside an SDK call surfaces as a JVM exception; a fault outside an SDK call ends the process.

On this page