Running Room Persistence Library queries on coroutines GlobalScope - android

I read that running routines on GlobalScope is bad practice.
What am doing now is:
class SplashScreen : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash_screen)
DataBaseHelper.isDbEmpty(this, object : DataBaseHelper.OnCompleteCheckDB {
override fun isDBEmpty(result: Boolean) {
//handle result
}
})
}
}
DatabseHelper:
class DataBaseHelper() {
companion object {
fun isDbEmpty(context: Context, param: OnCompleteCheckDB) {
val db = AppDatabase(context)
GlobalScope.launch(Dispatchers.IO) {
val count = db.movieDao().countElements() <= 0
withContext(Dispatchers.Main) {
param.isDBEmpty(count)
}
}
}
}
}
It works, but is it bad practice? What should I change if I wish to run it on the ActivityScope?

There'is lifecycleScope extension provided in lifecycle-runtime-ktx library, see Use Kotlin coroutines with Architecture components. Just add the library to your app's build.gradle
...
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
}
There's also viewModelScope available, if you're using ViewModel library. At first glance such a logic should be moved into a view model, so the query result will be retained during configuration changes. But let's use lifecycleScope since the question is about activity scope.
I've also replaced the callback by a suspend function. Coroutines are a great replacement for callbacks, so it's better to use coroutines when it's possible.
And one more thing, creating of a new AppDatabase instance multiple times doesn't look like a good idea. It's better to create it once and reuse it throughout your app. You can use Dependency Injection for that purpose, see Manage dependencies between components.
class DataBaseHelper() {
companion object {
suspend fun isDbEmpty(context: Context, param: OnCompleteCheckDB) = withContext(Dispatchers.IO) {
val db = AppDatabase(context)
db.movieDao().countElements() <= 0
}
}
}
class SplashScreen : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash_screen)
lifecycleScope.launch {
const dbIsEmpty = DataBaseHelper.isDbEmpty(this)
//handle result
}
}
}

Related

How to start a coroutine on main thread without using GlobalScope?

Whenever I want to start a coroutine on a main thread,
fun main(args: Array<String>) {
GlobalScope.launch {
suspededFunction()
}
}
suspend fun suspededFunction() {
delay(5000L) //heavy operation
}
GlobalScope is highlighted, and always taunt that its usage is delicate and require care.
What delicacies are involved with GlobalScope, and importantly how can I start a coroutine without using GlobalScope?
To start coroutine without using a GlobalScope, one can do as:
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
suspededFunction()
}
As mentioned in comments, some classes already have scopes available, like ViewModel class as viewModelScope.
in Activity or Fragment you can as follows:
//not recommended to use co-routines inside fragment or activity class
// this is just for example sack shown here.
// otherwise you have to do all your processing inside viewmodel
class Fragment : CoroutineScope by MainScope() {
...
override fun onDestroy() {
super.onDestroy()
cancel()
}
}
Kotlin already created some scope. you can use it according to your situation. and you also create your own scope. but I suggest in the beginning it is better to use that already created
check official documentation https://developer.android.com/topic/libraries/architecture/coroutines
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
lifecycleScope.launch {
//for activity
}
}
//for viewmodel
suspend fun abc() = viewModelScope.launch {
}

Why is it trying to access the database on the main thread?

I am getting this error while trying to display Room data in a LazyColumn in my project.
Cannot access database on the main thread since it may potentially lock the UI for a long period of time
I don't know why it is trying to access the database since I'm getting it with a ViewModelScope. Bellow is my MainActivity code and the ViewModel
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val users = viewModel.userList.value
LazyColumn(modifier = Modifier.fillMaxWidth()) {
items(users){data->
MyCard(data)
}
#HiltViewModel
class UserViewModel #Inject constructor(
private val repository: MainRepository
) : ViewModel() {
val userList: MutableState<List<User>> = mutableStateOf(listOf())
init {
viewModelScope.launch {
try {
val result: List<User> = repository.getAllUsers()
userList.value = result
} catch (e: Exception) {
Log.e("SSS", "${e.message.toString()}; ${e.stackTrace}")
}
}
}
Assuming you are following the pattern of your repository passing through functions from the DAO, you should mark this function in your DAO as a suspend function. This will cause it to automatically use a background thread. Then mark your corresponding repository function suspend so it can call the other suspend function.
Then in your coroutine, since getAllUsers() is a proper suspend function that internally handles proper use of background threads, there is nothing more you need to change.
The reason it gives you the warning is that by default, the viewModelScope runs on the main thread. It is up to you to wrap blocking calls in withContext(Dispatchers.IO) to run them in a background thread. But if you use suspend functions from the DAO, you don't have to worry about that because the function isn't blocking.
When launch { ... } is used without parameters, it inherits the
context (and thus dispatcher) from the CoroutineScope it is being
launched from.
Use IO thread to execute your code
viewModelScope.launch(Dispatchers.IO) {
try {
val result: List<User> = repository.getAllUsers()
userList.postValue(result)
} catch (e: Exception) {
Log.e("SSS", "${e.message.toString()}; ${e.stackTrace}")
}
}

Kotlin Flow: callbackFlow with lazy initializer of callback object

I want to use reactive paradigm using Kotlin Flow in my Android project. I have an external callback-based API so my choice is using callbackFlow in my Repository class.
I've already read insightfully some proper docs with no help:
callbackFlow documentation
Callbacks and Kotlin Flows by Roman Elizarov
What I want to achieve:
Currently my Repository class looks like this (simplified code):
lateinit var callback: ApiCallback
fun someFlow() = callbackFlow<SomeModel> {
callback = object : ApiCallback {
override fun someApiMethod() {
offer(SomeModel())
}
}
awaitClose { Log.d("Suspending flow until methods aren't invoked") }
}
suspend fun someUnfortunateCallbackDependentCall() {
externalApiClient.externalMethod(callback)
}
Problem occurs when someUnfortunateCallbackDependentCall is invoked faster than collecting someFlow().
For now to avoid UninitializedPropertyAccessException I added some delays in my coroutines before invoking someUnfortunateCallbackDependentCall but it is kind of hack/code smell for me.
My first idea was to use by lazy instead of lateinit var as this is what I want - lazy initialization of callback object. However, I couldn't manage to code it altogether. I want to emit/offer/send some data from someApiMethod to make a data flow but going outside of callbackFlow would require ProducerScope that is in it. And on the other hand, someUnfortunateCallbackDependentCall is not Kotlin Flow-based at all (could be suspended using Coroutines API at best).
Is it possible to do? Maybe using some others Kotlin delegates? Any help would be appreciated.
To answer your question technically, you can of course intialise a callback lazyily or with lateinit, but you can't do this AND share the coroutine scope (one for the Flow and one for the suspend function) at the same time - you need to build some kind of synchronisation yourself.
Below I've made some assumptions about what you are trying to achieve, perhaps they are not perfect for you, but hopefully give some incite into how to improve.
Since it is a Repository that you are creating, I will first assume that you are looking to store SomeModel and allow the rest of your app to observe changes to it. If so, the easiest way to do this is with a MutableStateFlow property instead of a callbackFlow:
interface Repository {
val state: Flow<SomeModel>
suspend fun reload()
}
class RepositoryImpl(private val service: ApiService) : Repository {
override val state = MutableStateFlow(SomeModel())
override suspend fun reload() {
return suspendCoroutine { continuation ->
service.callBackend(object : ApiCallback {
override fun someApiMethod(data: SomeModel) {
state.value = data
if (continuation.context.isActive)
continuation.resume(Unit)
}
})
}
}
}
interface ApiCallback {
fun someApiMethod(data: SomeModel)
}
data class SomeModel(val data: String = "")
interface ApiService {
fun callBackend(callback: ApiCallback)
}
The downside to this solution is that you have to call reload() in order to actually make a call to your backend, collecting the Flow alone is not enough.
myrepository.state.collect {}
myrepository.reload()
Another solution, again depending on what exactly you are trying to achieve, is to provide two ways to call your backend:
interface Repository {
fun someFlow(): Flow<SomeModel>
suspend fun reload(): SomeModel
}
class RepositoryImpl(private val service: ApiService) : Repository {
override fun someFlow() = callbackFlow<SomeModel> {
service.callBackend(object : ApiCallback {
override fun someApiMethod(data: SomeModel) {
offer(data)
}
})
awaitClose {
Log.d("TAG", "Callback Flow is closed")
}
}
override suspend fun reload(): SomeModel {
return suspendCoroutine<SomeModel> { continuation ->
service.callBackend(object : ApiCallback {
override fun someApiMethod(data: SomeModel) {
if (continuation.context.isActive)
continuation.resume(data)
}
})
}
}
}
interface ApiCallback {
fun someApiMethod(data: SomeModel)
}
data class SomeModel(val data: String = "")
interface ApiService {
fun callBackend(callback: ApiCallback)
}
Now you can either call reload() or someFlow() to retrieve SomeModel() and the Repository holds no "state".
Note that the reload() function is simply a 'coroutine' version of the callbackFlow idea.

Reference activity in koin Module

I have a single activity application.
My MainActivity is referenced in a number of dependency injection modules, as the implementer of these interfaces. I currently have a work around, which is less than ideal.
class MainActivity : TransaktActivity(), RegistrationNavigator, IAuthPresenter,
IAuthButtonNavigator {
override fun navigateAwayFromAuth() {
navController.navigate(R.id.homeFragment)
}
override fun navigateToAuthPin(buttonId: Int) {
//todo navigate to auth with pin fragment
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_mainActivity = this
setContentView(R.layout.activity_main)
}
companion object {
private var _mainActivity: MainActivity? = null
fun getInstance() = _mainActivity
}
}
interface RegistrationNavigator {
fun navigateToCardDetails()
fun navigateToOtpCapture()
fun navigateToLoading()
fun navigateOutOfCardRegistration()
}
The appModule is a Koin Module
val appModule = module {
viewModel { SharedViewModel() }
single { MainActivity.getInstance() as RegistrationNavigator }
}
What is the preferred way of achieving this?
Android-lifecycled components such as activities should not be in koin modules.
For example you will have issues with e.g. configuration changes since the koin module would be serving references to stale activity after the activity is recreated.
I haven't really worked with NavController but rather rolled up my own navigation solution. As a generic approach I would refactor the RegistrationNavigator implementation to a separate class the instance of which you can provide from your koin module. If lifecycle-dependent params such as Context (or NavController) are needed, supply them as function args.

Designing Modular Apps - Circular Dependency problem in navigation

As you know, designing Android app as modules is one of the popular practices nowadays in the Android development world. But this trend comes with some challenges. One of them is Circular Dependency.
For example, I have a navigation module which opens HomeActivity from Home Feature module. Also, I have to open another activity such as ProductListActivity from products module.
Home feature must include navigation module and navigation module should include HomeFeature if i navigate between activities like the following:
val intent = Intent(activity, HomeActivity::class.java)
This'll cause circular dependency problem.
For a fastest solution to figure out this problem is creating intents like the following and build navigation system on this approach.
Intent(Intent.ACTION_VIEW).setClassName(PACKAGE_NAME, className)
So my questions are, what other possible problems we'll face off with this navigation approach? Are there another practises to handle navigation in modular android apps?
Here is my solution for stiuation. This enables the use of explicit intents. You can also apply this approach to single activity application with navigation component with a little modification.
Here is navigation object for module B
object ModuleBNavigator {
internal lateinit var navigationImpl: ModuleBContract
fun setNavigationImpl(navigationImpl: ModuleBContract) {
this.navigationImpl = navigationImpl
}
interface ModuleBContract {
fun navigateModuleA(self: Activity, bundle: Bundle?)
}
}
And here is module B Activity
class ModuleBActivity : Activity() {
companion object {
private const val BUNDLE = "BUNDLE"
fun newIntent(context: Context, bundle: Bundle?) = Intent(context, ModuleBActivity::class.java).apply {
putExtra(BUNDLE, bundle)
}
}
}
And here is app module class to inject navigation impl to module A navigation object
class ApplicationModuleApp : Application() {
// Can also inject with a DI library
override fun onCreate() {
super.onCreate()
ModuleBNavigator.setNavigationImpl(object : ModuleBNavigator.ModuleBContract {
override fun navigateModuleA(self: Activity, bundle: Bundle?) {
self.startActivity(ModuleBActivity.newIntent(self, bundle))
}
})
}
}
And finally you can navigate from module A -> module B with provided implementation
class ModuleAActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ... Some code
ModuleBNavigator.navigationImpl.navigateModuleA(this, Bundle())
// .. Some code
}
}
This approact avoids circler dependency and you don't have to use implicit intents anymore.
Hope this helps.
For a different approach -actually similar which I mentioned in my question- which implementation belongs to sanogueralorenzo
Create a loader which laods the module classes
const val PACKAGE_NAME = "com.example.android"
private val classMap = mutableMapOf<String, Class<*>>()
private inline fun <reified T : Any> Any.castOrReturnNull() = this as? T
internal fun <T> String.loadClassOrReturnNull(): Class<out T>? =
classMap.getOrPut(this) {
try {
Class.forName(this)
} catch (e: ClassNotFoundException) {
return null
}
}.castOrReturnNull()
Create a String extension function for loading Intents dynamically.
private fun intentTo(className: String): Intent =
Intent(Intent.ACTION_VIEW).setClassName(BuildConfig.PACKAGE_NAME, className)
internal fun String.loadIntentOrReturnNull(): Intent? =
try {
Class.forName(this).run { intentTo(this#loadIntentOrReturnNull) }
} catch (e: ClassNotFoundException) {
null
}
Create another String extension function for loading Fragments dynamically
internal fun String.loadFragmentOrReturnNull(): Fragment? =
try {
this.loadClassOrReturnNull<Fragment>()?.newInstance()
} catch (e: ClassNotFoundException) {
null
}
Create an Feature interface for your feature implementations
interface Feature<T> {
val dynamicStart: T?
}
I assume that you have a Messages feature. Implement your dynamic feature interface
object Messages : Feature<Fragment> {
private const val MESSAGES = "$PACKAGE_NAME.messages.presentation.MessagesFragment"
override val dynamicStart: Fragment?
get() = MESSAGES.loadFragmentOrReturnNull()
}
And finally use it in another module without dependency
Messages.dynamicStart?.let {
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.fl_main, it)
.commit()
}
}

Categories

Resources