Display asset text file in Composable function using Kotlin Jetpack Compose

Viewed 969

I'm trying to read a text file using Kotlin from my Assets folder and display it to a Compose text widget. Android Studio Arctic Fox 2020.3

The following code runs successfully and displays the text file to the Output console, however I can't figure out how to get the text file and pass it to a Compose text widget.

You'll notice that I have 2 text() calls inside ReadDataFile(). The first text() is outside of try{} and it works fine, but the text() inside the try{} causes an error: "Try catch is not supported around composable function invocations"

How can I make this work?

Thanks!

package com.learning.kotlinreadfile

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import com.learning.kotlinreadfile.ui.theme.KotlinReadFileTheme
import java.io.InputStream
import java.io.IOException

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            KotlinReadFileTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    ReadDataFile()
                }
            }
        }
    }
}

@Composable
fun ReadDataFile() {
    println("Read Data File")
    Text("Read Data File")
    val context = LocalContext.current
    try {
        val inputStream: InputStream = context.assets.open("data.txt")
        val size: Int = inputStream.available()
        val buffer = ByteArray(size)
        inputStream.read(buffer)
        var string = String(buffer)
        println(string)
        //Text(string)      // ERROR: Try catch is not supported around composable function invocations
    } catch (e: IOException) {
        e.printStackTrace()
        println("Error")
    }
}
2 Answers

Caution

File read (I/O) operations can be long, so it is not recommended to use the UI scope to read files. But that's not what's causing the problem, I'm just warning you that if it reads very large files, it can make your app crash because it does a very long processing in the UI thread. I recommend checking this link if you're not familiar with this type of problem.

Problem solving following best practices

Fortunately Jetpack compose works very well with reactive programming, so we can take advantage of that to write reactive code that doesn't face the aforementioned problems. I made an example very similar to yours, I hope you can understand:

UiState file

As stated earlier, reading a file can be a long process, so let's imagine 3 possible states, "loading", "message successful" and "error". In the case of "successful message" we will have a possibly null string that will no longer be null when the message is actually read from the txt file:

package com.example.kotlinreadfile

data class UiState(
    val isLoading: Boolean,
    val isOnError: Boolean,
    val fileMessage: String?
)

MainActivity.kt file

Here will be just our implementation of the UI, in your case the text messages arranged in the application. As soon as we want to read these messages on the screen we will make a request to our ViewModel:

package com.example.kotlinreadfile

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.*
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.example.kotlinreadfile.ui.theme.KotlinReadFileTheme

class MainActivity : ComponentActivity() {

    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            KotlinReadFileTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    val context = LocalContext.current
                    viewModel.loadData(context)
                    ScreenContent(viewModel.uiState.value)
                }
            }
        }
    }

    @Composable
    fun ScreenContent(uiState: UiState) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
        ) {
            Text(text = "Read Data File")
            Spacer(modifier = Modifier.height(8.dp))
            when {
                uiState.isLoading -> CircularProgressIndicator()
                uiState.isOnError -> Text(text = "Error when try load data from txt file")
                else -> Text(text = "${uiState.fileMessage}")
            }
        }

    }
}

MainViewModel.kt file

If you are unfamiliar with the ViewModel class I recommend this official documentation link. Here we will focus on "our business rule", what we will actually do to get the data. Since we are dealing with an input/output (I/O) operation we will do this in an appropriate scope using viewModelScope.launch(Dispatchers.IO):

package com.example.kotlinreadfile

import android.content.Context
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.IOException
import java.io.InputStream

class MainViewModel : ViewModel() {
    private val _uiState = mutableStateOf(
        UiState(
            isLoading = true,
            isOnError = false,
            fileMessage = null
        )
    )
    val uiState: State<UiState> = _uiState

    fun loadData(context: Context) {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val inputStream: InputStream = context.assets.open("data.txt")
                val size: Int = inputStream.available()
                val buffer = ByteArray(size)
                inputStream.read(buffer)
                val string = String(buffer)
                launch(Dispatchers.Main) {
                    _uiState.value = uiState.value.copy(
                        isLoading = false,
                        isOnError = false,
                        fileMessage = string
                    )
                }
            } catch (e: IOException) {
                e.printStackTrace()
                launch(Dispatchers.Main) {
                    _uiState.value = uiState.value.copy(
                        isLoading = false,
                        isOnError = true,
                        fileMessage = null
                    )
                }
            }
        }
    }
}

jetpack compose by state refresh, please try

@Preview
@Composable
fun ReadDataFile() {
    var dataText by remember {
        mutableStateOf("asd")
    }
    println("Read Data File")
    Column {
        Text("Read Data File")
        Text(dataText)
    }
    val context = LocalContext.current
    LaunchedEffect(true) {
        kotlin.runCatching {
            val inputStream: InputStream = context.assets.open("data.txt")
            val size: Int = inputStream.available()
            val buffer = ByteArray(size)
            inputStream.read(buffer)
            String(buffer)
        }.onSuccess {
            it.logE()
            dataText = it
        }.onFailure {
            dataText = "error"
        }

    }
}
Related