Home > OS >  Jetpack Compose: calling action inside LauchedEffect blocks UI anyway
Jetpack Compose: calling action inside LauchedEffect blocks UI anyway

Time:12-30

I am trying to do some stuff on background and then displaying it to the user, but for some reason this does not work as it should and I am not sure what I am doing wrong.

It is an app with possibility to encrypt images and storing them on app-specific folder and holding the reference inside a database. While presenting it to the user following steps are done:

  1. Get the reference of pictures and metadata from database.
  2. read encrypted images and decrypt them while reading.
  3. Print the pictures in composable.

How it works is: Composable asks for getting data -> the repository gets the data -> my storage manager reads the files and uses the cryptomanager to decrypt them -> decrypted pictures are stored as live data

But the operation above blocks the interaction with the UI. Here is some Code:

Composable:

@Composable
fun WelcomeView(
    viewModel: WelcomeViewModel = hiltViewModel()
) {
    LaunchedEffect(Unit) {
        viewModel.getGalleryItems()
    }
    val list = viewModel.images.observeAsState()

    Column() {
//this button does not response until the data request and processing is done
        Button(onClick = {}){
            Text(text = "Click me while pictures are requested") 
        }
        LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
            if (list.value != null) {
                items(list.value as List<GalleryElement>) { item: GalleryElement ->
                    GalleryItem(element = item)
                }
            }
        }
    }
}

Thats the view model:

@HiltViewModel
class WelcomeViewModel @Inject constructor(
    private val secretDataManager: SecretDataManager,
) : ViewModel() {

    private val _images = MutableLiveData<List<GalleryElement>>()
    val images: LiveData<List<GalleryElement>> = _images

    suspend fun getGalleryItems() {
        viewModelScope.launch {
            _images.value = secretDataManager.getImages()
        }
    }
}

User data manager:

class SecretDataManager @Inject constructor(
    private val cryptoManager: CryptoManager,
    private val storageManager: StorageManager,
    private val repo: EncryptedVaultDataRepo,
    @ApplicationContext
    private val ctx: Context
) : SecretDataManagerService {

    override suspend fun getImages(): List<GalleryElement> {
        val result: MutableList<GalleryElement> = mutableListOf()
        repo.getAll().forEach {
            var image: ByteArray

            storageManager.readFile(File("${ctx.filesDir}/${it.name}").toUri()).use { b ->
                image = cryptoManager.decrypt(it.iv, b?.readBytes()!!)
            }

            result.add(GalleryElement(BitmapFactory.decodeByteArray(image, 0, image.size)))
        }
        return result
    }
}

Any ideas what I am doing wrong?

CodePudding user response:

I believe the main problem is that the viewModelScope.launch(){} starts on the Dispatchers.Main(UI) thread. I recommend going to viewModelScope.launch(Dispatchers.IO){}. I am trying to find the documentation to support that but should be an easy change. I also recommended populating the list on the initialization of the view model.

@Composable
fun WelcomeView(
    viewModel: WelcomeViewModel = hiltViewModel()
) {
    val list = viewModel.images.observeAsState()

    Column() {
//this button does not response until the data request and processing is done
        Button(onClick = {}){
            Text(text = "Click me while pictures are requested") 
        }
        LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
            if (list.value != null) {
                items(list.value as List<GalleryElement>) { item: GalleryElement ->
                    GalleryItem(element = item)
                }
            }
        }
    }
}

@HiltViewModel
class WelcomeViewModel @Inject constructor(
    private val secretDataManager: SecretDataManager,
) : ViewModel() {

    private val _images = MutableLiveData<List<GalleryElement>>()
    val images: LiveData<List<GalleryElement>> = _images


    init{
       getGalleryImages()
    }
    
    fun getGalleryItems() {
        viewModelScope.launch(Dispatchers.Default) {
            _images.value = secretDataManager.getImages()
        }
    }
}
  • Related