I Built a Button That Rewrites Text in Any Tone. Now My App Sounds Like a CEO!

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.]
1*3LMWFkJPqO8G98xjU17_3Q.png

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.

πŸ”— Full project: https://github.com/josegbel/smart-writer

πŸ“Έ 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:

  1. My neighbour is not a very good cook 🍳 and he never cleans his kitchen 🧽.
My neighbour is not a very good cook πŸ˜” and 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!

stat



I Built a Button That Rewrites Text in Any Tone. Now My App Sounds Like a CEO! 😎 was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.

Continue reading...
 


Join 𝕋𝕄𝕋 on Telegram
Channel PREVIEW:
Back
Top