Still a bit new to Kotlin and coroutines, so I want to learn best practices. The following code seems to work as expected in it's original context, though the naming has been tweaked here. The list of widgets is retrieved from the database, and the UI is updated correctly.
My main question is, in updateList
is it approprate to use withContext (Dispatchers.Main)
to access the UI from a job on the IO context? Feels a bit kludgy at first blush, but it does seem to work as expected.
class WidgetListActivity : AppCompatActivity() {
private lateinit var db: AppDatabase
private lateinit var getListJob: Job
private lateinit var widgetList: List<WidgetRecord>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_widget_list)
runBlocking {
db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "Widget.db")
.createFromAsset("database/prepop_db.sqlite").build()
}
val recyclerView = findViewById<RecyclerView>(R.id.widget_recycler_view)
val linearLayoutManager = LinearLayoutManager(this.applicationContext)
recyclerView.layoutManager = linearLayoutManager
updateList()
}
private fun updateList() {
getListJob = GlobalScope.launch (Dispatchers.IO) {
widgetList = db.widgetDao().getAllWidgets()
// HERE'S THE SECTION IN QUESTION
withContext(Dispatchers.Main) {
val recyclerView = findViewById<RecyclerView>(R.id.deck_recycler_view)
var widgetListAdapter = WidgetListAdapter(widgetList)
recyclerView.adapter = widgetListAdapter
widgetListAdapter.notifyDataSetChanged()
}
}
}
override fun onDestroy() {
runBlocking {
getListJob.join()
}
// PLEASE NOTE: I know you wouldn't normally destroy the database
// like this. The whole point of this Activity is simply to test
// loading and displaying the contents of a prepopulated database.
this.applicationContext.deleteDatabase("Widgets.db")
super.onDestroy()
}
}
1 Answer 1
One thing that makes it kludgy is using GlobalScope, which is discouraged by the coroutines design lead. In this case, if this were a very long-running job, you would be leaking your UI elements because they are captured by the coroutine.
Also, since you are using Room, you can make your DAO's getAllWidgets()
a suspend function, in which case, there's no reason to use a specific dispatcher to call it. A properly composed suspend function doesn't block the dispatcher it is called from, so you never should have to specify a dispatcher to call a suspend function. The proper way to do this would be:
private fun updateList() {
getListJob = lifecycleScope.launch {
widgetList = db.widgetDao().getAllWidgets()
val recyclerView = findViewById<RecyclerView>(R.id.deck_recycler_view)
var widgetListAdapter = WidgetListAdapter(widgetList)
recyclerView.adapter = widgetListAdapter
widgetListAdapter.notifyDataSetChanged()
}
}
No Dispatcher specifying needed at all, because the coroutine doesn't call any blocking code and lifecycleScope
defaults to Dispatchers.Main
.
-
\$\begingroup\$ Thanks so much for the info. There are a couple of things in particular in your answer that I simply wasn't aware of. I didn't realize there was a
lifeCycleScope
available as a member. That's very handy. I also didn't realize that a dispatcher isn't needed to call asuspend
function. I think the relationships between dispatchers, coroutine scopes, and suspending functions have been the hardest part to wrap my head around, but this information helps a lot. Cheers. \$\endgroup\$steve_79– steve_792021年01月13日 16:43:52 +00:00Commented Jan 13, 2021 at 16:43 -
1\$\begingroup\$ Also, just to note for anyone who reads this later, you have to add
lifecycle-runtime
as a dependency in build.gradle to use thelifecycleScope
member. https://developer.android.com/jetpack/androidx/releases/lifecycle \$\endgroup\$steve_79– steve_792021年01月13日 17:06:01 +00:00Commented Jan 13, 2021 at 17:06 -
\$\begingroup\$ Yes, it took a while for it all to click for me because there are so many components to a coroutine. \$\endgroup\$Tenfour04– Tenfour042021年01月13日 17:25:25 +00:00Commented Jan 13, 2021 at 17:25
-
1\$\begingroup\$ If you start a job in lifecycleScope, its because you are fetching something for that Activity or Fragment, so presumably you don't care about the result and want it to be cancelled when the Activity or Fragment is destroyed. The cancellation is done automatically because lifecycleScope is automatically cancelled in the onDestroy phase. Joining it waits for the result, but there's no point if you are just going to throw the result away. If you join, you are holding the Activity/Fragment and everything it references in memory until the job is done. \$\endgroup\$Tenfour04– Tenfour042021年01月13日 19:05:06 +00:00Commented Jan 13, 2021 at 19:05
-
1\$\begingroup\$ Also,
runBlocking
has no place anywhere in an Android application. Its only two intended purposes are for use in unit tests and directly in themain
function of a JVM app. Blocking a main thread locks the UI and can cause an ANR error. \$\endgroup\$Tenfour04– Tenfour042021年01月13日 19:07:25 +00:00Commented Jan 13, 2021 at 19:07