-
Notifications
You must be signed in to change notification settings - Fork 4
Description
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
generateContenttogenerateContentStream- 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
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)
}
}