2

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?

WinterMute
  • 145
  • 2
  • 11
  • I am confused on the question here? Is the problem that it does not load or that the items load but on the UI thread when you want it to be on the background? – Kerry Dec 29 '22 at 21:22
  • "Important: Using suspend doesn't tell Kotlin to run a function on a background thread. It's normal for suspend functions to operate on the main thread. It's also common to launch coroutines on the main thread. You should always use withContext() inside a suspend function when you need main-safety, such as when reading from or writing to disk, performing network operations, or running CPU-intensive operations." https://developer.android.com/kotlin/coroutines/coroutines-adv – Zun Jan 04 '23 at 14:48
  • Good article about viewModel scope https://medium.com/androiddevelopers/easy-coroutines-in-android-viewmodelscope-25bffb605471 – bitvale Jan 06 '23 at 22:21

1 Answers1

2

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()
        }
    }
}
Kerry
  • 283
  • 2
  • 16
  • You are completely right. That is the reason, but I found only another Stackoverflow thread where I came across the information. It is somehow not documented clearly enough, that the viewmodelscope.launch works per default on mainthread. Thank you – WinterMute Dec 29 '22 at 21:41
  • Alternatively define your dispatcher in your repo, for example using `withContext(Dispatchers.IO) {}` (make the dispatcher a class variable so it can be tested properly). It makes sense to display stuff through the Main dispatcher but get data from IO) – Zun Jan 04 '23 at 14:46