Reputation: 7179
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
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