Jetpack Compose: calling action inside LauchedEffect blocks UI anyway - android

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:
Get the reference of pictures and metadata from database.
read encrypted images and decrypt them while reading.
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?

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

LazyColumn is not updated when the data is changed

I just started learning Android development. I want to find a case, using Viewmodel+Room+Flow+collectAsStateWithLifecycle(), unfortunately I didn't find such a case. So I wrote the following code referring to the documentation and some information on the Internet.
Dao
#Dao
interface IArticleDao {
#Query("select * from article")
fun getAll(): List<Article>
#Insert
suspend fun add(article: Article): Long
}
Repository
private const val TAG = "ArticleRepository"
class ArticleRepository #Inject constructor(
private val articleDao: IArticleDao
) {
private val ioDispatcher = IO
fun add(article: Article): Long {
var id = 0L
CoroutineScope(ioDispatcher).launch {
try {
id = articleDao.add(article)
}catch (e: Exception){
Log.e(TAG, "There is an exception in the new article")
}
}
return id
}
fun getAll(): Flow<List<Article>> =
flow {
emit(
articleDao.getAll()
)
}.flowOn(ioDispatcher)
}
ViewModel
#HiltViewModel
class ArticleViewModel #Inject constructor(private val articleRepository: ArticleRepository) : ViewModel() {
val articles = articleRepository.getAll().stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList()
)
fun add(article: Article): Long{
return articleRepository.add(article)
}
}
Activity
Scaffold(
content = { innerPadding ->
val articles by articleViewModel.articles.collectAsStateWithLifecycle()
Content(
articles = articles, articleViewModel
)
}
)
#Composable
fun Content(
articles:
List<Article>, viewModel: ArticleViewModel
) {
Column {
Row {
Button(onClick = {
viewModel.add(Article(title = "this is a test", url = "http://www.test4.com/"))
}) {
Text("add")
}
}
Row {
LazyColumn(
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(16.dp)
) {
items(items = articles, key = { article -> article.id }) { article ->
Row {
Text(text = article.title)
}
}
}
}
}
}
This creates a problem. When a new piece of data is added, a new piece of data is added to the database, but the UI interface is not updated accordingly. Only when the Home button is clicked and the application is opened again can the UI interface be updated.
I know I must be missing something, but I don't know what. Possibly the code in the Repository?
Instead of returning a list in your DAO, try returning a Flow<List<Article>>.
To do that, don't forget to add the following dependency implementation("androidx.room:room-ktx:$room_version")
#Dao
interface IArticleDao {
#Query("select * from article")
fun getAll(): Flow<List<Article>>
#Insert
suspend fun add(article: Article): Long
}
private const val TAG = "ArticleRepository"
class ArticleRepository #Inject constructor(
private val articleDao: IArticleDao
) {
private val ioDispatcher = IO
fun add(article: Article): Long {
var id = 0L
CoroutineScope(ioDispatcher).launch {
try {
id = articleDao.add(article)
}catch (e: Exception){
Log.e(TAG, "There is an exception in the new article")
}
}
return id
}
fun getAll(): Flow<List<Article>> = articleDao.getAll().flowOn(ioDispatcher)
}
Using a flow, you will listen the database changes instead of just querying one time the datas.

How to read from DataStore Preferences to string?

Im trying to use datastore inside Composable to read user data but cant read the value as string to put inside Text.
That's the datastore
private val Context.userPreferencesDataStore: DataStore<Preferences> by preferencesDataStore(
name = "user"
)
private val USER_FIRST_NAME = stringPreferencesKey("user_first_name")
suspend fun saveUserToPreferencesStore(context: Context) {
context.userPreferencesDataStore.edit { preferences ->
preferences[USER_FIRST_NAME] = "user1"
}
}
fun getUserFromPreferencesStore(context: Context): Flow<String> = context.userPreferencesDataStore.data
.map { preferences ->
preferences[USER_FIRST_NAME] ?: ""
}
and inside Composable:
#Composable
fun myComposable() {
var context = LocalContext.current
LaunchedEffect( true){
saveUserToPreferencesStore(context )
}
Text(getUserFromPreferencesStore(context ))
}
so in your code, getUserFromPreferencesStore() is returning a Flow. so you should collect that as flow, and then compose will auto update once the data is being changed. For example (something similar to this):
val user by getUserFromPreferencesStore(context).collectAsStateWithLifecycleAware(initValue)

Saving recyclerview items with savestate

I'm currently writing an app that displays a list of movies. The app has 8 fragments that contain the recyclerview: Trending Movies, Action, Comedy, Horror, Romance, Scifi, Search, and Favorites.
The items in the recyclerview contain a checkbox that adds the movie to the favorites. When I scroll or exit the app, the checkbox state resets. I'm trying to save the state of the checkbox using savestate but it's not working.
Can anyone please tell me what I'm doing wrong? Below is the viewmodel.
Thank you.
MoviesListViewModel.kt
package com.example.moviesapp.ui
import androidx.lifecycle.*
import com.example.moviesapp.network.MoviesRepository
import com.example.moviesapp.network.MoviesResults
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
const val DEFAULT_QUERY = " "
const val ACTION_MOVIES = "moviesAction"
const val COMEDY_MOVIES = "moviesComedy"
const val HORROR_MOVIES = "moviesHorror"
const val ROMANCE_MOVIES = "moviesRomance"
const val SCIFI_MOVIES = "moviesScifi"
const val TRENDING_MOVIES = "moviesTrending"
enum class MovieApiStatus {LOADING, ERROR, DONE}
#HiltViewModel
class MoviesListViewModel #Inject constructor(
private val repository: MoviesRepository,
private var state: SavedStateHandle
): ViewModel() {
private val _moviesAction: MutableLiveData<List<MoviesResults.Movies>> = state.getLiveData(ACTION_MOVIES)
val moviesAction: LiveData<List<MoviesResults.Movies>> = _moviesAction
private val _moviesComedy: MutableLiveData<List<MoviesResults.Movies>> = state.getLiveData(COMEDY_MOVIES)
val moviesComedy: LiveData<List<MoviesResults.Movies>> = _moviesComedy
private val _moviesHorror: MutableLiveData<List<MoviesResults.Movies>> = state.getLiveData(HORROR_MOVIES)
val moviesHorror: LiveData<List<MoviesResults.Movies>> = _moviesHorror
private val _moviesRomance: MutableLiveData<List<MoviesResults.Movies>> = state.getLiveData(
ROMANCE_MOVIES)
val moviesRomance: LiveData<List<MoviesResults.Movies>> = _moviesRomance
private val _moviesScifi: MutableLiveData<List<MoviesResults.Movies>> = state.getLiveData(SCIFI_MOVIES)
val moviesScifi: LiveData<List<MoviesResults.Movies>> = _moviesScifi
private val _moviesTrending: MutableLiveData<List<MoviesResults.Movies>> = state.getLiveData(TRENDING_MOVIES)
val moviesTrending: LiveData<List<MoviesResults.Movies>> = _moviesTrending
private val _networkState = MutableLiveData<MovieApiStatus>()
val networkState: LiveData<MovieApiStatus> = _networkState
init {
getMovies()
}
fun getAction() {
viewModelScope.launch {
_moviesAction.value = repository.getActionMovies()
}
}
fun getComedy() {
viewModelScope.launch {
_moviesComedy.value = repository.getComedyMovies()
}
}
fun getHorror() {
viewModelScope.launch {
_moviesHorror.value = repository.getHorrorMovies()
}
}
fun getRomance() {
viewModelScope.launch {
_moviesRomance.value = repository.getRomanceMovies()
}
}
fun getScifi() {
viewModelScope.launch {
_moviesScifi.value = repository.getScifiMovies()
}
}
fun getTrending() {
viewModelScope.launch {
_moviesTrending.value = repository.getTrendingMovies()
}
}
private var currentQuery = MutableLiveData(DEFAULT_QUERY)
val movies = currentQuery.switchMap {
queryString ->
liveData {
emit(repository.getSearchResults(queryString))
}
}
fun searchMovies(query: String) {
currentQuery.value = query
}
private fun getMovies() {
viewModelScope. launch {
_networkState.value = MovieApiStatus.LOADING
try {
_networkState.value = MovieApiStatus.DONE
}
catch (e: Exception) {
_networkState.value = MovieApiStatus.ERROR
}
}
}
class MoviesListViewModelFactory #Inject constructor(private val repository: MoviesRepository, private val state: SavedStateHandle): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MoviesListViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return MoviesListViewModel(repository, state) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
}
Interface for accessing and modifying preference data returned by Context.getSharedPreferences(String, int). For any particular set of preferences, there is a single instance of this class that all clients share. Modifications to the preferences must go through an Editor object to ensure the preference values remain in a consistent state and control when they are committed to storage. Objects that are returned from the various get methods must be treated as immutable by the application.
Note: This class provides strong consistency guarantees. It is using expensive operations which might slow down an app. Frequently changing properties or properties where loss can be tolerated should use other mechanisms. For more details read the comments on Editor.commit() and Editor.apply().
Note: This class does not support use across multiple processes.
in your case you can simply store the key-value pair to record the relevant selection.

Compose use Flow<T>.collectAsState render List<T> LazyColumn progressive

I have a file list, need query data from each file, then render Result list to LazyColumn
But I dont want render list until all the query finished, like this:
class Repo {
private val list = listOf(fileA, fileB)
fun query(key: String): Flow<List<Result>>
= list.asFlow().map{ it.query(key) }.toList()
}
#Composable
fun ListView(repo: Repo){
val res = repo.query("xxx").collectAsState(xxx)
LazyColumn{
items(res){ xxx }
}
}
How can I render a LazyColumn from Flow.collectAsState progressive
class Repo {
private val list = listOf(fileA, fileB)
fun query(key: String): Flow<Result>
= list.asFlow().map{ it.query(key) }
}
#Composable
fun ListView(repo: Repo){
val res = repo.query("xxx").collectAsState(xxx)
LazyColumn{
// ???
items(res){ xxx }
}
}
Just found derivedStateOf function work perfect for this.
class Repo {
private val list = listOf(fileA,fileB)
fun query(key: String): Flow<Result> = list.asFlow().map{xxx}
}
#Composable
fun ListView(repo: Repo){
val flow = repo.query("xxx").collectAsState(xxx)
val update by remember {
val data = mutableListOf<Result>()
derivedStateOf {
flow?.run(data::add)
data
}
}
LazyColumn {
items(update) { data: Result ->
xxxx
}
}
}

Android Kotlin coroutines create RecyclerView on fragment open with variable

As told in the title i try to create a recyclerview on fragment open with a variable.
Here is a working version without variable:
Fragment:
viewModel.lists.observe(viewLifecycleOwner) {
listAdapter.submitList(it)
}
ViewHolder:
val lists = shishaDao.getList(HARDCODED_INT).asLiveData()
As you can see, there is an hardcoded integer. This integer can hold different values, which is changing the lists value.
Here is my try with a variable:
Fragment:
viewModel.lists(title).observe(viewLifecycleOwner) {
listAdapter.submitList(it)
}
Instead of accessing a variable of the viewholder I am now wanna access a function, which needs the neccessary variable.
ViewHolder:
fun lists(title: String): LiveData<List<Tabak>> {
val nr = dao.getNr(title)
return dao.getList(nr).asLiveData()
}
The App is crashing with following error:
Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
Here are also tried ways:
fun lists(title: String): LiveData<List<Tabak>> {
val nr: Int = 0
viewModelScope.launch {
nr = dao.getNr(title)
Log.e("NR", nr.toString())
}
return dao.getList(nr).asLiveData()
}
fun lists(title: String): LiveData<List<Tabak>> {
val nr: MutableLiveData<Int> = MutableLiveData(0)
viewModelScope.launch {
nr.value = dao.getNr(title)
Log.e("NR", nr.value.toString())
}
return dao.getList(nr.value!!).asLiveData()
}
Both methods do not crash. The Log.e display the right number, but the last line still uses the 0.
My actual question: How can i get thi nr value from dao.getNr(title) to use it in the last line getList(nr)?
i found a way using LiveData. This is a big way, but a useful as well.
new PreferenceManager.kt
class PreferencesManager #Inject constructor(context: Context) {
private val dataStore = context.createDataStore("user_preferences")
val preferencesFlow = dataStore.data
.catch { exception ->
if (exception is IOException) {
Log.e(TAG, "Error reading preferences", exception)
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val choseMarke = preferences[PreferencesKeys.CHOSE_TITLE] ?: 2
FilterPreference(choseTitle)
}
suspend fun updateChoseTitle (choseTitle: Int) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.CHOSE_TITLE] = choseTitle
}
}
private object PreferencesKeys {
val CHOSE_TITLE = preferencesKey<Int>("chosen_title")
}
}
Fragment.kt looks again like this
viewModel.lists.observe(viewLifecycleOwner) {
listAdapter.submitList(it)
}
And the ViewModel.kt
class ListsViewModel #ViewModelInject constructor(private val dao: Dao, private val preferencesManager: PreferencesManager, #Assisted private val state: SavedStateHandle) : ViewModel() {
val searchQuery = state.getLiveData("searchTitle", "")
val preferencesFlow = preferencesManager.preferencesFlow
...
private val listsFlow = combine(searchQuery.asFlow(), preferencesFlow) { query, filterPreference ->
Pair(query, filterPreference)
}.flatMapLatest { (_, filterPreference) ->
dao.getList(filterPreference.choseMarke)
}
I am not more trying to use the title inside of the fragment. Before opening the fragment, i need to press a button which this title, and then I already save the title (as int from the dao getNr) inside the mainActivity.
MainActivity.kt
onNavigationItemSelected(item: MenuItem): Boolean {
viewModel.onNavigationClicked(item)
}
MainViewModel.kt
fun onNavigationClicked(item: MenuItem) = viewModelScope.launch {
val choseMarkeNr = shishaDao.getMarkeNr(item.title.toString())
preferencesManager.updateChoseMarke(choseMarkeNr)
}
This way is working. :)

Categories

Resources