J
Jose Garcia
Guest
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.]
![[TrendyMediaToday.com] Googleโs AI Just Proofread My Writing Better Than I Ever Could {file_size} {filename} [TrendyMediaToday.com] Googleโs AI Just Proofread My Writing Better Than I Ever Could {file_size} {filename}](https://cdn-images-1.medium.com/max/800/1*Ud3Unq7-h_90xCPAzujcXg.png)
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.

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) }
}
}
}
}
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:

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.
Googleโs AI Just Proofread My Writing Better Than I Ever Could

Continue reading...