6

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
  • 1
    Try to put //Text(string) after try/ catch. Also, wrap it Column Commented Jan 9, 2022 at 21:12
  • @Alexander: That did it! Commented Jan 10, 2022 at 0:40

2 Answers 2

13

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
                    )
                }
            }
        }
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

Thank you for your excellent post enhancing the solution. I understand WHY you think this approach is important and I'm going to try to learn HOW to do it from your post.
Calling viewModel.loadData(context) in a composable is a bad idea: This will be triggered on each recomposition. Moving this further up out of the composable scope to onCreate solves this issue. (Otherwise, use a LaunchedEffect)
4

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"
        }

    }
}

3 Comments

Thank you for your code! That works too. I had to comment out the "it.logE()" line because that wasn't recognized. What was that supposed to do?
Does the code by YellowSea solve the long file read problems that @PierreVieira raises in the submission above this one?
sorry. logE() is my Log Print . you can delete or add fun String.logE(tag: String = "TAG") = Log.e(tag, this) @SqueezeOJ

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.