Skip to content

Sharing experience #2

@Puzzak

Description

@Puzzak

Heyo. I don't want to fork/pull directly because what I did with the plugin changes the logic pretty severely.
There are couple of quirks (like I implemented summarization via custom streaming), but overall changes are:

  • Move from the normal generateContent to generateContentStream
    • Basically this means that all interactions with model are now streamed. Instead of "send request, wait for response" loop, I implemented "send request, gather fragments and update the UI untill you receive all-done from AI Core"
  • Added several features, as example a way to get model download status (also streamed) so the ui can be updated
Image

Full disclosure, I am not good with Kotlin, so it was a long and painfull process, while I learned relevant Kotlin with the help of AI. There may be some errors or arhcitectural issues, but overall it looks okay-ish. I am willing to give what I learned back to you since if this will be helpful for you, it will benefit everyone.

My changes are in 2 files, one is the Kotlin plugin, one is the slightly reworked dart connector that communicates with it. They are attached.

This here is the Dart side of the plugin
gemini.dart.txt
And this is the plugin itself
FlutterLocalAiPlugin.kt.txt

Main changes are with initAiCore:

val status = generativeModel!!.checkStatus()
            when (status) {
                FeatureStatus.AVAILABLE -> "Available=Available=0"
                FeatureStatus.UNAVAILABLE -> "Error=AICore=Unavailable"
                FeatureStatus.DOWNLOADING -> "Download=In Progress=0"
                FeatureStatus.DOWNLOADABLE -> {
                    var downloadMessage = "Download=In Progress=0"
                    generativeModel!!.download().collect {
                        status -> when (status) {
                            is DownloadStatus.DownloadStarted ->   downloadMessage = "Download=In Progress=0"
                            is DownloadStatus.DownloadProgress ->  downloadMessage = "Download=In Progress=${status.totalBytesDownloaded}"
                            is DownloadStatus.DownloadCompleted -> downloadMessage = "Available=Download=0"
                            is DownloadStatus.DownloadFailed ->    downloadMessage = "Error=Download=${status.e.message}"
                            else ->                                downloadMessage = "Error=Download=Unknown"
                        }
                    }
                    downloadMessage
                }
                else -> "Error=Unknown=$status"
            }

The statuses are then split by = and didplayed in the app.

In the generateTextStream (new one):

return generativeModel!!.generateContentStream(request)
            .transform { chunk ->
                val newChunkText = chunk.candidates.firstOrNull()?.text ?: ""
                val finishReason = chunk.candidates.firstOrNull()?.finishReason.toString()
                fullResponse += newChunkText
                val generationTime = System.currentTimeMillis() - startTime
                val tokenCount = fullResponse.split(" ").filter { it.isNotEmpty() }.size
                emit(mapOf(
                    "text" to fullResponse,
                    "chunk" to newChunkText,
                    "generationTimeMs" to generationTime,
                    "tokenCount" to tokenCount,
                    "reason" to finishReason
                ))
            }

I am now working with chunks, which allows me to update the chat immediately after receiving the text part instead of waiting for the whole text to arrive.

In the handleGenerateTextStream I use coroutine to continuously monitor the status of each chunk and relay it to the Dart code:

coroutineScope.launch {
            try {
                withContext(Dispatchers.Main) {
                    events.success(mapOf("status" to "Loading", "response" to null, "error" to null))
                }

                generateTextStream(prompt, configMap)
                    .onEach { chunkMap ->
                        val reasonFromChunk = chunkMap["reason"] as? String
                        if (reasonFromChunk != null && reasonFromChunk != "null") {
                            lastFinishReason = reasonFromChunk
                        }
                        withContext(Dispatchers.Main) {
                            events.success(mapOf("status" to "Streaming", "response" to chunkMap, "error" to null))
                        }
                    }
                    .onCompletion {
                        withContext(Dispatchers.Main) {
                            events.success(mapOf("status" to "Done", "response" to null, "error" to null, "reason" to lastFinishReason))
                            events.endOfStream()
                        }
                    }
                    .catch { e ->
                        withContext(Dispatchers.Main) {
                            events.success(mapOf("status" to "Error", "response" to null, "error" to e.message))
                            events.endOfStream()
                        }
                    }
                    .launchIn(this)

            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    events.success(mapOf("status" to "Error", "response" to null, "error" to e.message))
                    events.endOfStream()
                }
            }
        }

This is all sent via EventChannel and received via MethodChannel:

override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
        if (events == null) return

        val args = arguments as? Map<String, Any>
        val method = args?.get("method") as? String
        val payload = args?.get("payload") as? Map<String, Any>

        if (method == null || payload == null) {
            events.error("INVALID_ARGS", "Missing 'method' or 'payload' in EventChannel arguments", null)
            return
        }

        when (method) {
            "generateText" -> handleGenerateText(payload, events)
            "generateTextStream" -> handleGenerateTextStream(payload, events)
            else -> events.error("UNKNOWN_METHOD", "Unknown method '$method' for EventChannel", null)
        }
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions