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

Part 3 of the series: Rewriting text with ML Kit on Android
After adding Summarisation and Proofreading to SmartWriter, the next logical step was toneβshifting. Thanks to ML Kitβs Rewriting API I had a working feature in under an hourβββmost of the boilerplate was copyβpaste from the previous ViewModels.
Available rewrite styles: Elaborate, Emojify, Shorten, Friendly, Professional, and Rephrase. SmartWriter supports all the available styles.
ML Kit supports EN, JA, DE, FR, IT, ES and KO for rewriting, but SmartWriter currently ships English only.

What it looks like
This is what the rewriting feature did when I chose to rewrite my input using the Emojify writing style.
Input: My neighbour is not a very good cook and he never cleans his kitchen.
Outputs:
- My neighbour is not a very good cook
and he never cleans his kitchen
.
My neighbour is not a very good cookand he never cleans his kitchen
.
Thatβs pretty cool, huh!
Dependency
If you want to take advantage of the rewriting feature you will have to add the following dependency to your project:
mlkit-genai-rewriting = "com.google.mlkit:genai-rewriting:1.0.0-beta1"
Core ViewModel logic
The ViewModel is the most important when it comes to implementing this. It will be in charge of calling the API and exposing the results. Below are the three key areas that matter most:
1. What happens when the user taps Rewrite
fun onRewriteClicked(context: Context) {
viewModelScope.launch {
try {
val options = RewriterOptions.builder(context)
.setOutputType(uiState.value.selectedOutputType.value) // ELABORATE, EMOJIFY, etc.
.setLanguage(RewriterOptions.Language.ENGLISH)
.build()
rewriter = Rewriting.getClient(options) // Creates the onβdevice client
prepareAndStartRewriting() // Jump to featureβstatus handling
} catch (e: Exception) {
_uiEvent.emit(RewritingUiEvent.Error("Error: ${e.message}"))
}
}
}
- We build RewriterOptions with the chosen style.
- Rewriting.getClient(options) gives us the Gemini Nano client.
- We delegate to prepareAndStartRewriting() to deal with model availability.
2. Handling model download
suspend fun prepareAndStartRewriting() {
when (rewriter?.checkFeatureStatus()?.await()) {
FeatureStatus.DOWNLOADABLE -> downloadFeature()
FeatureStatus.DOWNLOADING -> { /* keep spinner, weβll retry */ }
FeatureStatus.AVAILABLE -> startRewritingRequest(uiState.value.inputText, rewriter!!)
else -> _uiEvent.emit(RewritingUiEvent.Error("Device unsupported"))
}
}
- DOWNLOADABLE β triggers downloadFeature(); spinner on.
- DOWNLOADING β already in progressβββUI stays loading.
- AVAILABLE β jump straight to the inference call.
- UNAVAILABLE β show an error.
Download callbacks update isLoading and, once complete, retry the same text automatically.
private fun downloadFeature() {
rewriter?.downloadFeature(
object : DownloadCallback {
override fun onDownloadStarted(bytesToDownload: Long) {
_uiState.update { it.copy(isLoading = true) }
}
override fun onDownloadProgress(totalBytesDownloaded: Long) {
_uiState.update { it.copy(isLoading = true) }
// totalBytesDownloaded useful to display an accurate progress bar
}
override fun onDownloadCompleted() {
_uiState.update { it.copy(isLoading = false) }
rewriter?.let { startRewritingRequest(uiState.value.inputText, it) }
}
override fun onDownloadFailed(e: GenAiException) {
_uiState.update { it.copy(isLoading = false) }
_uiEvent.tryEmit(
RewritingUiEvent.Error(
message = "Download failed: ${e.message}",
),
)
}
},
)
}
The important part of the downloadFeature function is seeing how the rewritingRequest will start when the download is completed I guess.
We could also show an accurate progressbar using the onDownloadStarted and onDownloadProgress callback methods by processing the values of bytesToDownload and etotalBytesDownloaded.
3. Running inference (startRewritingRequest)
fun startRewritingRequest(text: String, rewriter: Rewriter) {
val request = RewritingRequest.builder(text).build()
_uiState.update { it.copy(isLoading = true) }
viewModelScope.launch {
try {
val results = rewriter.runInference(request).await().results
_uiState.update { state ->
state.copy(correctionSuggestions = results.map { it.text })
}
} catch (e: Exception) {
_uiEvent.emit(RewritingUiEvent.Error("Error during rewriting: ${e.message}"))
} finally {
_uiState.update { it.copy(isLoading = false) }
}
}
}
- Wrap the user text in RewritingRequest.
- runInference(...).await() returns alternative sentences already rewritten in the chosen style.
- We map them to plain strings and push them into correctionSuggestions so the UI can list them.
Exposing data to the UI
private val _uiState = MutableStateFlow(RewritingUiState())
val uiState: StateFlow<RewritingUiState> = _uiState.asStateFlow()
RewritingUiState holds:
- inputText β original text
- selectedOutputType β current style (Friendly, Professional β¦)
- isLoading β drives the spinner
- correctionSuggestions β rewritten alternatives to display
Errors flow through _uiEvent: SharedFlow<RewritingUiEvent> so Compose can show snackbars without muddying state.
Why this was quick
The skeleton is almost identical to Summarisation and Proofreading: swap the client, swap the request, add a style enum. Thatβs it.
Next up
With Rewriting done, the last stop in the series is Image Description: generating altβtext from photos without hitting the cloud. Stay tuned and donβt forget to follow the rest of this series by clicking here.!
Also, if you found this useful, it would help massively if you clapped to this article and follow me for more articles like this!
Thank you!
I Built a Button That Rewrites Text in Any Tone. Now My App Sounds Like a CEO!

Continue reading...