dbarnes
dbarnes

Reputation: 529

How do you use Compose LazyColumn with Coroutines/Room Database?

I'm writing a simple app for myself to display an RSS feed as a list of items and figured it'd be a good opportunity to learn about @Compose lazyColumn. I'm blown away with how little code it is, no Adapter, no ViewHolder, no xml. Amazingly streamlined.

But how can you set it up to work with something like Room Database where you're using a coroutine to pull data from a database? The official documentation here talks about rememberCoroutineScope() but doesn't explain where you define it. Placing a coroutine inside the Surface definition feels weird, which is probably why it doesn't work. It yields a: @Composable invocations can only happen from the context of a @Composable function error.

Does this need to be done through a ViewModel? In the real world data from the repository will be coming from a ViewModel anyway, but for now I just wanted to display a simple of list of items pulled from the internet (ie, requiring a coroutine). But then how do you handle LiveData or List<String> from ViewModel/Database?

Does anyone know how to set this up? I feel it shouldn't be that hard, but I don't know where to start looking for answers.

Ex:

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           MyAppTheme {

               val composeScope = rememberCoroutineScope()
               Surface(color = MaterialTheme.colors.background) {
  
                    composeScope.launch{ // do network call
                        RssList(RssFetcher.fetchRss())  // how do you make this work?
                    }
               }
           }
       }
   }
}

@Composable
fun RssList(list: List<RssItems>){
   LazyColumn{ items(list) ... }
}

Upvotes: 4

Views: 3106

Answers (2)

Richard Onslow Roper
Richard Onslow Roper

Reputation: 6835

Create a viewmodel, ideally, assuming you might want to preserve the downloaded feed for future use.

class RSSViewModel : ViewModel() {
    var RSS by mutableStateOf(listOf</*Add Type Here*/>()) // could be val as well
        private set // No external modifications to protect from side-effects

    init {
        viewModelScope.launch { // Pre-defined Coroutine Scope
            RSS =
                RSSFetcher.fetchRSS() // You could also extract the logic into a method for on-demand initialisation
        }
    }

    fun refreshRSS() { // This is what I was talking about above. On-demand
        viewModelScope.launch { 
            RSS =
                RSSFetcher.fetchRSS()
        }
    }
}

Now, note we have declared the variable of type MutableState<T> which is necessary in Compose to trigger recompositions (You could also use LiveData or Flow, but I strongly recommend using this as much as you can, offers high reliability).

Ok, we're all set up here, now just add code to the activity

class MainActivity : ComponentActivity() {
    private val viewModel by viewModels<RSSViewModel>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAppTheme {
               RSSList(viewModel.RSS) // Automatic Recompositions on Refresh, all done!
            }
        }
    }
}

Yep,

EDIT: If you wish to extract the feed from a Room Database though, it would be a heck of a mess if you return raw data as it is. The Exceptions will say you cannot call this on the main thread; then add those thread(){...} calls all over the place which makes it look messier than your room. For that, return LiveData instead. Surprisingly, if you return LiveData from the database queries, you actually can call the methods on the main thread. Clean.

Upvotes: 2

Phil Dukhov
Phil Dukhov

Reputation: 87794

@Composable
fun TestView(
) {
    var rssList by remember { mutableStateOf(emptyList<RssItems>()) }
    LaunchedEffect(Unit) {
        rssList = RssFetcher.fetchRss()
    }
    RssList(rssList) 
}

@Composable
fun RssList(list: List<RssItems>){
    LazyColumn{ items(list) ... }
}

To understand what's going on here you need to check out about:

  1. Storing state in compose. remember { mutableStateOf() } is the simplest way of storing data between recompositions. But it'll be cleaned when you switch between views, so check out on view models too, which may suit better for your task. documentation
  2. LaunchedEffect is the preferred way to do any actions inside composable functions. Inside this block you're already in a coroutine, so can run suspend functions
  3. You don't need to define a coroutine for rememberCoroutineScope, it returns pre-initialized coroutine. Usually you need to use it for events like button press or touch

Upvotes: 5

Related Questions