Skip to main content
Latest version: v0.9.7
The LEAP SDK is now a Kotlin Multiplatform library supporting Android, iOS, macOS, and JVM. While Android is well-tested and production-ready, other platforms are currently in testing.

1. Prerequisites​

You should already have:
  • An Android project created in Android Studio. You may create an empty project with the wizard. LEAP Android SDK is Kotlin-first. We recommend to work with the SDK only in Kotlin.
  • Leap Android SDK needs Kotlin Android plugin v2.3.0 or above and Android Gradle Plugin v8.13.0 or above to build. Declare it in your root build.gradle.kts as:
    plugins {
        id("com.android.application") version "8.13.2" apply false
        id("com.android.library") version "8.13.2" apply false
        id("org.jetbrains.kotlin.android") version "2.3.10" apply false
    }
    
  • A working Android device that supports arm64-v8a ABI with developer mode enabled. We recommend having 3GB+ of RAM to run the models.
  • The minimal SDK requirement is API 31. Declare it in build.gradle.kts as
    android { defaultConfig {  minSdk = 31  targetSdk = 36 }}
    
The SDK may crash on loading model bundles on emulators. A physical Android device is recommended.

2. Configure Permissions​

The LeapModelDownloader runs as a foreground service and displays notifications during downloads. You need to add the following permissions to your AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

    <application ...>
        <!-- Your activities -->
    </application>
</manifest>
The POST_NOTIFICATIONS permission requires a runtime permission request on Android 13 (API 33) and above. See the code example in step 3 for how to request this permission.

3. Import the LeapSDK​

Add the following dependencies into $PROJECT_ROOT/app/build.gradle.kts: Option A: Direct dependency declaration
dependencies {
  implementation("ai.liquid.leap:leap-sdk:0.9.7")
  implementation("ai.liquid.leap:leap-model-downloader:0.9.7") // Android-specific model downloader
}
Option B: Version catalog (recommended) In gradle/libs.versions.toml:
[versions]
leapSdk = "0.9.7"

[libraries]
leap-sdk = { module = "ai.liquid.leap:leap-sdk", version.ref = "leapSdk" }
leap-model-downloader = { module = "ai.liquid.leap:leap-model-downloader", version.ref = "leapSdk" }
Then in app/build.gradle.kts:
dependencies {
  implementation(libs.leap.sdk)
  implementation(libs.leap.model.downloader)
}
Then perform a project sync in Android Studio to fetch the LeapSDK artifacts.
For Android, we recommend using the leap-model-downloader module which provides background downloads with WorkManager and notification support. Other platforms (iOS, macOS, JVM) should use the LeapDownloader class from the core leap-sdk module.

4. Getting and Loading Models​

The SDK uses GGUF manifests for loading models (recommended for all new projects due to superior inference performance and better default generation parameters).
Legacy Executorch bundle support is available in the accordion below for existing projects.

Loading from GGUF Manifest

The LEAP Edge SDK supports directly downloading LEAP models in GGUF format. Given the model name and quantization method (which you can find in the LEAP Model Library), the SDK will automatically download the necessary GGUF files along with generation parameters for optimal performance. For Android, LeapModelDownloader provides the best experience with background downloads, WorkManager integration, and notification support. This function takes some time to finish as loading the model is a heavy I/O operation, but it is safe to call on the main thread. The function should be executed in a coroutine scope. ViewModel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import ai.liquid.leap.Conversation
import ai.liquid.leap.ModelRunner
import ai.liquid.leap.model_downloader.LeapModelDownloader
import ai.liquid.leap.model_downloader.LeapModelDownloaderNotificationConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

class ChatViewModel(application: Application) : AndroidViewModel(application) {
    private val modelDownloader = LeapModelDownloader(
        application,
        notificationConfig = LeapModelDownloaderNotificationConfig.build {
            notificationTitleDownloading = "Downloading AI model..."
            notificationTitleDownloaded = "Model ready!"
            notificationContentDownloading = "Please wait while the model downloads"
        }
    )

    private var modelRunner: ModelRunner? = null
    private var conversation: Conversation? = null

    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()

    private val _downloadProgress = MutableStateFlow(0f)
    val downloadProgress: StateFlow<Float> = _downloadProgress.asStateFlow()

    private val _errorMessage = MutableStateFlow<String?>(null)
    val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow()

    fun loadModel() {
        viewModelScope.launch {
            _isLoading.value = true
            _errorMessage.value = null
            try {
                modelRunner = modelDownloader.loadModel(
                    modelSlug = "LFM2-1.2B",
                    quantizationSlug = "Q5_K_M",
                    progress = { progressData ->
                        _downloadProgress.value = progressData.progress
                    }
                )
                conversation = modelRunner?.createConversation()
                _isLoading.value = false
            } catch (e: Exception) {
                _errorMessage.value = "Failed to load model: ${e.message}"
                _isLoading.value = false
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        // Use runBlocking to ensure model is unloaded before ViewModel is destroyed
        // viewModelScope is cancelled during clearing, so we need a non-cancelled context
        runBlocking(Dispatchers.IO) {
            modelRunner?.unload()
        }
    }
}
Activity
import android.os.Build
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import android.content.pm.PackageManager
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
    private val viewModel: ChatViewModel by viewModels()

    // Permission launcher for POST_NOTIFICATIONS
    private val requestPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        if (isGranted) {
            // Permission granted
            viewModel.loadModel()
        } else {
            // Permission denied - downloads will work but without notifications
            viewModel.loadModel()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Observe loading state and download progress
        lifecycleScope.launch {
            viewModel.isLoading.collect { isLoading ->
                // Update UI loading indicator
            }
        }

        lifecycleScope.launch {
            viewModel.downloadProgress.collect { progress ->
                // Update download progress UI (0.0 to 1.0)
            }
        }

        lifecycleScope.launch {
            viewModel.errorMessage.collect { error ->
                error?.let {
                    // Show error message to user
                }
            }
        }

        // Request notification permission and load model
        checkPermissionsAndLoadModel()
    }

    private fun checkPermissionsAndLoadModel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            when {
                ContextCompat.checkSelfPermission(
                    this,
                    android.Manifest.permission.POST_NOTIFICATIONS
                ) == PackageManager.PERMISSION_GRANTED -> {
                    // Permission already granted
                    viewModel.loadModel()
                }
                else -> {
                    // Request permission
                    requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
                }
            }
        } else {
            // No permission needed for Android 12 and below
            viewModel.loadModel()
        }
    }
}
The SDK will automatically download the required GGUF files to the device’s cache and load the model with the appropriate generation parameters specified in the manifest.
For cross-platform projects or if you don’t need Android-specific features, you can use the LeapDownloader class from the core leap-sdk module:
import ai.liquid.leap.LeapDownloader
import ai.liquid.leap.LeapDownloaderConfig

lifecycleScope.launch {
  try {
    val baseDir = File(context.filesDir, "model_files").absolutePath
    val modelDownloader = LeapDownloader(config = LeapDownloaderConfig(saveDir = baseDir))
    val modelRunner = modelDownloader.loadModel(
        modelSlug = "LFM2-1.2B",
        quantizationSlug = "Q5_K_M"
    )
  } catch (e: LeapModelLoadingException) {
    Log.e(TAG, "Failed to load the model. Error message: ${e.message}")
  }
}
This approach works on all platforms (Android, iOS, macOS, JVM) but doesn’t provide Android-specific features like background downloads or notifications.
Browse the Leap Model Library to find and download a model bundle that matches your needs.

Download and transfer bundle

Push the bundle file to the device using adb push. Assuming the downloaded model file is located at ~/Downloads/model.bundle, run the following commands:
adb shell mkdir -p /data/local/tmp/leap
adb push ~/Downloads/model.bundle /data/local/tmp/leap/model.bundle

Loading from local bundle file

The LeapClient.loadModel suspend function loads a model bundle file and returns a model runner instance for invoking the model. This function takes some time to finish as loading the model is a heavy I/O operation, but it is safe to call on the main thread. The function should be executed in a coroutine scope.
lifecycleScope.launch {
  try {
    modelRunner = LeapClient.loadModel("/data/local/tmp/leap/model.bundle")
  }
  catch (e: LeapModelLoadingException) {
    Log.e(TAG, "Failed to load the model. Error message: ${e.message}")
  }
}

5. Generate content with the model​

To generate content, use the conversation object created in the ViewModel. The Conversation.generateResponse function returns a Kotlin asynchronous flow of MessageResponse, which can be processed with Kotlin flow operators. Updated ViewModel with generation
import ai.liquid.leap.MessageResponse
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach

class ChatViewModel(application: Application) : AndroidViewModel(application) {
    // ... previous code ...

    private val _responseText = MutableStateFlow("")
    val responseText: StateFlow<String> = _responseText.asStateFlow()

    private val _isGenerating = MutableStateFlow(false)
    val isGenerating: StateFlow<Boolean> = _isGenerating.asStateFlow()

    private var generationJob: Job? = null

    fun generateResponse(userMessage: String) {
        // Cancel any ongoing generation
        generationJob?.cancel()

        viewModelScope.launch {
            _isGenerating.value = true
            _responseText.value = ""
            _errorMessage.value = null

            conversation?.generateResponse(userMessage)
                ?.onEach { response ->
                    when (response) {
                        is MessageResponse.Chunk -> {
                            // Append text chunk to response
                            _responseText.value += response.text
                        }
                        is MessageResponse.ReasoningChunk -> {
                            // Handle reasoning/thinking chunks separately
                            Log.d(TAG, "Reasoning: ${response.text}")
                        }
                        is MessageResponse.Complete -> {
                            // Generation completed
                            Log.d(TAG, "Generation done. Stats: ${response.stats}")
                        }
                        else -> {
                            // Handle other response types (FunctionCalls, AudioSample, etc.)
                        }
                    }
                }
                ?.onCompletion {
                    _isGenerating.value = false
                    Log.d(TAG, "Generation completed")
                }
                ?.catch { exception ->
                    _errorMessage.value = "Generation failed: ${exception.message}"
                    _isGenerating.value = false
                }
                ?.collect()
        }.also { job ->
            generationJob = job
        }
    }

    fun stopGeneration() {
        generationJob?.cancel()
        _isGenerating.value = false
    }

    companion object {
        private const val TAG = "ChatViewModel"
    }
}
Activity UI Integration
class MainActivity : AppCompatActivity() {
    private val viewModel: ChatViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ... previous code ...

        // Observe response text
        lifecycleScope.launch {
            viewModel.responseText.collect { text ->
                // Update UI with generated text
                textView.text = text
            }
        }

        // Observe generation state
        lifecycleScope.launch {
            viewModel.isGenerating.collect { isGenerating ->
                // Update UI: disable send button, show progress, etc.
                sendButton.isEnabled = !isGenerating
            }
        }

        // Send message when button clicked
        sendButton.setOnClickListener {
            val userInput = inputEditText.text.toString()
            if (userInput.isNotBlank()) {
                viewModel.generateResponse(userInput)
                inputEditText.text.clear()
            }
        }

        // Stop generation when stop button clicked
        stopButton.setOnClickListener {
            viewModel.stopGeneration()
        }
    }
}
In this pattern:
  • onEach is called when the model generates a chunk of content
  • onCompletion is called when generation finishes - at this point, conversation.history contains the complete conversation
  • catch handles any exceptions during generation
  • Call stopGeneration() to cancel an ongoing generation

6. Examples​

See LeapSDK-Examples for complete example apps using LeapSDK. Edit this page