Cilenco
Cilenco

Reputation: 7179

Persistence file operations in background

Currently I'm developing a persistence library for Android in Kotlin. Now I am at the point where I've to handle file operations (read, write, etc.) and I wonder what is the best way to do this? First I don't have to do this on the main thread to block the UI. Second I have to be sure that all operations are executed immediately and no one gets lost at process kills or device restarts.

I looked at the background guide on the developer site but now I am a bit confused. As I don't want to start a forground service for each persist operation it looks like WorkManager is the best solution for me. The documentation states:

WorkManager is intended for tasks that are deferrable - that is, not required to run immediately - and required to run reliably even if the app exits or the device restarts.

But here is the problem: My work should be executed immediately and does not depend on system events so I am not sure if this is the best way to go. What do you thing is the best solution for me?

Upvotes: 1

Views: 419

Answers (1)

CommonsWare
CommonsWare

Reputation: 1007474

You can have a repository expose suspend functions that handle the I/O. Here is a TextRepository that reads and writes text from a Uri (as this is adapted from a future book sample that supports files and Storage Access Framework Uri values):

class TextRepository(context: Context) {
  private val resolver: ContentResolver = context.contentResolver

  suspend fun read(source: Uri) = withContext(Dispatchers.IO) {
    try {
      resolver.openInputStream(source)?.use { stream ->
        StreamResult.Content(source, stream.readText())
      } ?: throw IllegalStateException("could not open $source")
    } catch (e: FileNotFoundException) {
      StreamResult.Content(source, "")
    } catch (t: Throwable) {
      StreamResult.Error(t)
    }
  }

  suspend fun write(source: Uri, text: String): StreamResult =
    withContext(Dispatchers.IO) {
      try {
        resolver.openOutputStream(source)?.use { stream ->
          stream.writeText(text)
          StreamResult.Content(source, text)
        } ?: throw IllegalStateException("could not open $source")
      } catch (t: Throwable) {
        StreamResult.Error(t)
      }
    }
}

private fun InputStream.readText(charset: Charset = Charsets.UTF_8): String =
  readBytes().toString(charset)

private fun OutputStream.writeText(
  text: String,
  charset: Charset = Charsets.UTF_8
): Unit = write(text.toByteArray(charset))

sealed class StreamResult {
  object Loading : StreamResult()
  data class Content(val source: Uri, val text: String) : StreamResult()
  data class Error(val throwable: Throwable) : StreamResult()
}

In this case, I am using the loading-content-error (LCE) pattern, where the suspend functions are returning a StreamResult. StreamResult.Content wraps the read-in text or the text that was just written.

Then, you can have a ViewModel of some sort call the suspend functions:

class MainMotor(repo: TextRepository) : ViewModel {
  private val _results = MutableLiveData<StreamResult>()
  val results: LiveData<StreamResult> = _results

  fun read(source: Uri) {
    _results.value = StreamResult.Loading

    viewModelScope.launch(Dispatchers.Main) {
      _results.value = repo.read(source)
    }
  }

  fun write(source: Uri, text: String) {
    _results.value = StreamResult.Loading

    viewModelScope.launch(Dispatchers.Main) {
      _results.value = repo.write(source, text)
    }
  }
}

In my case, I route the StreamResult unmodified through a MutableLiveData for the UI to consume, following an MVI-style pattern. In practice, a ViewModel probably transforms the repo result into something more directly usable by the UI, and so the LiveData would be of some other type, with the ViewModel performing the conversion.

Upvotes: 2

Related Questions