Auto-saving drafts in Kotlin for Android

Automatically saving content as it’s written is an essential part of the user experience. It’s used everywhere from Medium to Microsoft Word. Here’s how to implement this in Kotlin for Android.

Photo by Markus Spiske on Unsplash

The Plan

To create a seamless user experience, we want to save content as user types. Rather than saving after every character that is typed, it would be best to wait for a pause in the typing and save each time the typing stops.

To do this we need three things. Firstly, a user content form. Secondly, a way of determining when the user is typing and thirdly a way of sending the content to our server for the draft to be stored in our database.

Creating the content form

The styling of the content form is very much a design choice, but fundamentally it will be an EditText component. This component will allow the user to type long-form text. I’ve used the example below:

<EditText
android:id="@+id/edit_story"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginHorizontal="24dp"
android:background="@android:color/transparent"
android:layout_marginVertical="24dp"
android:hint="Start writing your story.."
android:inputType="textCapSentences|textAutoCorrect|textMultiLine"
app:fontFamily="@font/belgrano" />

Creating our SaveDraft function

To save the draft, we need to send the data to our server to be stored in a database table. To keep it simple, we will send the full content every time but if your users are writing particularly large documents then it may be beneficial to optimize this by only sending additions to the existing text.

fun saveDraft(text: String){
user?.getIdToken(true)
?.addOnCompleteListener { task ->
if (task.isSuccessful) {
val idToken: String? = task.result?.token
val uid = FirebaseAuth.getInstance().currentUser?.uid
Fuel.post("./saveDraft.php", listOf("uid" to uid, "idToken" to idToken, "creationDate" to creationDate, "text" to text, "title" to title)).responseString { request, response, result ->
val (saveResult, error) = result
if (saveResult == "Success"){
Snackbar.make(findViewById<EditText>(R.id.edit_story), "Saved", Snackbar.LENGTH_LONG).show()
}
else
{
Log.e(TAG, "Failed to save user story.")
}
}
} else {
Log.e(TAG, "Failed to generate user token")
}
}
}

Here we are using FirebaseAuth for our authentication and sending the details to the server to be validated along with the user draft. We are also using Fuel to manage our HTTPS request to send the data to the server. We are sending 5 variables to the server:

  • uid and idToken are authentication variables for FirebaseAuth
  • creationDate is used as an identifier for the draft so that a user can maintain multiple drafts at the same time. We can set this with val creationDate = LocalDateTime.now()
  • text and title are the values for the content that we are saving.

How to check if the user has stopped typing?

To figure out when a pause occurs in the user’s typing, we need to flip this question around and detect when the user starts typing. We can then set a timer which will save our draft after a given time, say one second. If we detect that the user presses another key within that one second then we cancel our action to save the draft. This prevents sending data after every keystroke, which is wasteful but still saves the content after each flurry of activity.

To do this we need to create a Handler to run our SaveDraft function after a short delay. We can then initiate this every time the user presses a key and cancel it if another keystroke follows within this short time period.

val saveDraftHandler = Handler()

Once we have created this handler, we then need to detect user keystrokes and use this to initiate and cancel our handler.

We do this using a TextWatcher component. This can be added to an EditText using a lambda function as below:

findViewById<EditText>(R.id.edit_story).addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(p0: Editable?) {

if (p0.toString() != "") {
findViewById<EditText>(R.id.edit_story).hint = ""
saveDraftHandler.removeCallbacksAndMessages(null);
saveDraftHandler.postDelayed({ saveDraft(p0.toString()) }, 1000)
}
}


override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}

override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}

})

The TextWatcher object is created and added as a Listener for when the text changes within the EditText. We must override the three functions which comprise a TextWatcher but the main one we are interested in for now is afterTextChanged(). This is called after every keystroke the user makes within that text box.

Within this function we are using removeCallbacksAndMessages() to cancel the Handler to prevent it running the saveDraft activity if it is currently scheduled.

Then we are rescheduling the activity with postDelayed() to run the saveDraft activity after 1000ms.

If the user is typing a stream of text then the handler keeps getting cancelled and rescheduled until the user eventually stops typing for more than a second, at which point the saveDraft activity is run and the file is saved.

We have implemented the Runnable within the postDelayed() function as a lambda which in turn calls our saveDraft() function. We could have built the saveDraft() functionality directly into this lambda, however then we wouldn’t have been able to call saveDraft() in other contexts so we have decomposed these elements.

Conclusion

Creating an auto-saving form in Kotlin is very simple using a TextWatcher and the postDelayed functionality of a Handler. To improve this and make it more efficient, one could reduce the amount of data transmitted by checking if this is simply appending text to the previous draft.

You could do this by storing lastDraft = p0.text and running the comparison if(lastDraft == p0.text.take(lastDraft.length) to check if the two start in the same way. This would give you something like the below:

if(lastDraft == text.take(lastDraft.length)){
val appendText = text.drop(lastDraft.length)
}

More advanced solutions are possible for optimising the data transfer, let me know if you come up with an interesting one.

Markets and Technology

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store