Blog Infos
Author
Published
Topics
, , , ,
Published

This is Part 2 of a series of articles where I explain how to implement GenAI on Android. [Click here to view the full series.]

Let’s test out the proofreading feature on MLKit

After building the summarisation feature in SmartWriter, integrating proofreading was incredibly fast — I reused almost all the same logic and had a working feature in under an hour.

This time, instead of focusing on UI, I’ll walk you through how the entire ViewModel works — including model download handling, inference, and graceful error fallback — using the new Proofreading API from ML Kit GenAI.

👉 Full app available here:

https://github.com/josegbel/smartwriter

📌 What we’re building

We’re using ML Kit’s on-device GenAI model to take raw user input and return grammatically improved alternatives. For example:

Input:

i think this idea could works, but not sure if make sense

Output:

I think this idea could work, but I’m not sure if it makes sense.

Let’s look at how the ViewModel makes this possible — no Compose needed to understand this one 😉

🧠 The ViewModel in full

Here’s the complete code for the ProofreadingViewModel. I’ll break it down below:

package com.example.smartwriter.viewmodel

import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.smartwriter.ui.model.ProofreadingUiEvent
import com.example.smartwriter.ui.model.ProofreadingUiState
import com.google.mlkit.genai.common.DownloadCallback
import com.google.mlkit.genai.common.FeatureStatus
import com.google.mlkit.genai.common.GenAiException
import com.google.mlkit.genai.proofreading.Proofreader
import com.google.mlkit.genai.proofreading.ProofreaderOptions
import com.google.mlkit.genai.proofreading.Proofreading
import com.google.mlkit.genai.proofreading.ProofreadingRequest
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.launch
import javax.inject.Inject

class ProofreadingViewModel
    @Inject
    constructor() : ViewModel() {

    companion object {
        private val TAG = ProofreadingViewModel::class.java.simpleName
    }

    private val _uiState = MutableStateFlow(ProofreadingUiState())
    val uiState: StateFlow<ProofreadingUiState> = _uiState.asStateFlow()

    private val _uiEvent = MutableSharedFlow<ProofreadingUiEvent>()
    val uiEvent: SharedFlow<ProofreadingUiEvent> = _uiEvent.asSharedFlow()

    private var proofreader: Proofreader? = null

    override fun onCleared() {
        proofreader?.close()
        super.onCleared()
    }

    fun onInputTextChanged(newText: String) {
        _uiState.update { it.copy(inputText = newText) }
    }

    fun onProofreadClicked(context: Context) {
        viewModelScope.launch {
            try {
                val options =
                    ProofreaderOptions
                        .builder(context)
                        .setInputType(ProofreaderOptions.InputType.KEYBOARD)
                        .setLanguage(ProofreaderOptions.Language.ENGLISH)
                        .build()

                proofreader = Proofreading.getClient(options)

                prepareAndStartProofreading()
            } catch (e: Exception) {
                Log.e(TAG, "Error in onProofreadClicked: ${e.message}", e)
                _uiEvent.emit(ProofreadingUiEvent.Error(message = "Error: ${e.message}"))
            }
        }
    }
🔍 Breakdown — Initialisation
  • We use ProofreaderOptions to configure the input type (keyboard text vs voice) and language (English).
  • Proofreading.getClient(options) creates the proofreader instance.
  • Then we delegate to prepareAndStartProofreading().

 

suspend fun prepareAndStartProofreading() {
    val featureStatus = proofreader?.checkFeatureStatus()?.await()
    Log.d(TAG, "Feature status: $featureStatus")

    when (featureStatus) {
        FeatureStatus.DOWNLOADABLE -> {
            Log.d(TAG, "Feature DOWNLOADABLE – starting download")
            downloadFeature()
        }

        FeatureStatus.DOWNLOADING -> {
            Log.d(TAG, "Feature DOWNLOADING – will start once ready")
            proofreader?.let { startProofreadingRequest(uiState.value.inputText, it) }
        }

        FeatureStatus.AVAILABLE -> {
            Log.d(TAG, "Feature AVAILABLE – running inference")
            _uiState.update { it.copy(isLoading = true) }
            proofreader?.let {
                Log.d(TAG, "starting proofreading request")
                startProofreadingRequest(uiState.value.inputText, it)
            }
        }

        FeatureStatus.UNAVAILABLE, null -> {
            Log.e(TAG, "Feature UNAVAILABLE")
            _uiEvent.emit(ProofreadingUiEvent.Error(message = "Your device does not support this feature."))
        }
    }
}

 

🔍 Breakdown — Feature availability
  • checkFeatureStatus() tells us whether the Gemini Nano model is available, downloading, or needs to be downloaded.
  • If it’s downloadable, we call downloadFeature().
  • If it’s already available, we immediately run the inference.
  • We also handle DOWNLOADING and UNAVAILABLE.

This flexibility ensures the user gets feedback even when the model isn’t ready yet.

private fun downloadFeature() {
    proofreader?.downloadFeature(
        object : DownloadCallback {
            override fun onDownloadStarted(bytesToDownload: Long) {
                _uiState.update { it.copy(isLoading = true) }
                Log.d(TAG, "Download started – bytesToDownload=$bytesToDownload")
            }

            override fun onDownloadProgress(totalBytesDownloaded: Long) {
                _uiState.update { it.copy(isLoading = true) }
                Log.d(TAG, "Download progress – totalBytesDownloaded=$totalBytesDownloaded")
            }

            override fun onDownloadCompleted() {
                _uiState.update { it.copy(isLoading = false) }
                Log.d(TAG, "Download completed – starting inference")
                proofreader?.let { startProofreadingRequest(uiState.value.inputText, it) }
            }

            override fun onDownloadFailed(e: GenAiException) {
                _uiState.update { it.copy(isLoading = false) }
                Log.e(TAG, "Download failed: ${e.message}", e)
                _uiEvent.tryEmit(
                    ProofreadingUiEvent.Error(
                        message = "Download failed: ${e.message}",
                    ),
                )
            }
        },
    )
}
🔍 Breakdown — Download logic
  • This handles model download events and updates the UI accordingly.
  • On successful download, we trigger the inference again — using the same input.

 

 fun startProofreadingRequest(
        text: String,
        proofreader: Proofreader,
    ) {
        val proofreadingRequest = ProofreadingRequest.builder(text).build()
        _uiState.update { it.copy(isLoading = true) }

        viewModelScope.launch {
            try {
                val results = proofreader.runInference(proofreadingRequest).await().results
                _uiState.update { state ->
                    state.copy(correctionSuggestions = results.map { it.text })
                }
            } catch (e: Exception) {
                _uiEvent.emit(
                    ProofreadingUiEvent.Error(
                        message = "Error during proofreading: ${e.message}",
                    ),
                )
            } finally {
                _uiState.update { it.copy(isLoading = false) }
            }
        }
    }
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

🔍 Breakdown — Running the model
  • The input string is wrapped in a ProofreadingRequest.
  • runInference(…).await() gives us the corrected suggestions.
  • We extract and display result.text for each suggestion in the UI.
🧪 Key takeaways
  • ✅ If you’ve implemented summarisation already, adding proofreading is fast.
  • ✅ The Gemini Nano model gives usable, human-like corrections.
  • 🧠 You get multiple suggestions, not diffs — you can choose how to present them.
  • 📵 Emulators and unsupported phones won’t work. I tested on a Galaxy S25 Ultra.
  • 🌍 Currently only supports English.
🚀 Try it yourself

This is Part 2 of my SmartWriter blog series. You can find the full app (including Compose UI and previews) here:

👉 https://github.com/josegbel/smartwriter

Next up: Text Rewriting — where we’ll teach the app to rephrase user input in different tones and lengths.

Follow along and let me know what you’d improve.

This article was previously published on proandroiddev.com.

Menu