How can I call a composable function outside of an activity? - android

In my case I would like to call the composable from a OneTimeWorkRequest.
As far as I'm aware composables are typically called from Activities with setContent(). However I do not have access to this within the Worker and not sure how to instantiate a 'blank/dummy' activity within the worker.
#Composable
fun Hello() {
Text(text = "Hello")
}
class MyWorker(private val appContext: Context, private val params: WorkerParameters)
: CoroutineWorker(appContext,params) {
override suspend fun doWork(): Result {
Hello() /*<---error here "#Composable invocations can only happen from the context of a #Composable function"*/
return Result.success()
}
}

Related

Jetpack Compose - rememberCoroutineScope but with keys

How can I obtain a coroutine scope bound to a composable but also to some key values? Basically I want to obtain something like this:
#Composable
fun Sth(val sth: Int) {
val coroutineScope = rememberCoroutineScope(sth)
}
I need the scope to be canceled when the call leaves the composition (just like with rememberCoroutineScope), but also when the key sth changes.
Update:
One place in which I need this functionality:
class SomeIndication(
val a: Int,
val b: Int
) : Indication {
#Composable
override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
val coroutineScope = rememberCoroutineScope(interactionSource)
return remember(interactionSource) {
val sth: State<Int> = sth(a, b, coroutineScope)
object: IndicationInstance {
override fun ContentDrawScope.drawIndication() {
drawContent()
drawSomething(x.value)
}
}
}
}
}
Try to use LaunchedEffect:
#Composable
fun Sth(val sth: Int) {
// `LaunchedEffect` will cancel and re-launch if `sth` changes
LaunchedEffect(sth) {
// call suspend functions
}
}
When LaunchedEffect enters the Composition, it launches a coroutine
with the block of code passed as a parameter. The coroutine will be
cancelled if LaunchedEffect leaves the composition. If
LaunchedEffect is recomposed with different keys, the existing
coroutine will be cancelled and the new suspend function will be
launched in a new coroutine.
Or try to wrap launching a coroutine with a LaunchedEffect:
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(key1 = sth) {
// will be canceled and re-launched if sth is changed
coroutineScope.launch() {
// call suspend functions
}
}

How to access other custom method outside work manager?

I am working on app in which first i have to start the process and then update the value so how to access other methods of custom method o workmanager thanks
class SmsWorkManager(val context : Context, workerParameters:WorkerParameters) : CoroutineWorker(context ,workerParameters) {
override suspend fun doWork(): Result {
println("do some task ")}
fun updateMethod(){
println("how to access this method")}
}
// class Instannce for work maanager
val workManager = WorkManager.getInstance(this )
// val oneTimeRequest =OneTimeWorkRequest.Builder(SmsWorkManager::class.java)
workManager.enqueue(oneTimeRequest.build())
You need to return a Result after work completion & you can simply use the updateMethod() inside your `doWork() like below:
class SmsWorkManager(val context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
println("do some task ")
updateMethod()
return Result.success()
}
fun updateMethod(){
println("how to access this method")
}
}
Also, if you are not doing any IO task then you should use a Worker instead of a CoroutineWorker.

How to manage coroutines in MainActivity with waiting until done?

I use GlobalScope with runBlocking in MainActivity, but I do not use there a flow just suspend function. I would like to change GlobalScope to other Scope from Coroutines.
UseCase
class UpdateNotificationListItemUseCase #Inject constructor(private val notificationDao: NotificationDao): BaseUpdateBooleanUseCase<Int, Boolean, Boolean, Boolean, Unit>() {
override suspend fun create(itemId: Int, isRead: Boolean, isArchived: Boolean, isAccepted: Boolean){
notificationDao.updateBooleans(itemId, isRead, isArchived, isAccepted)
}
}
MainActivity
val job = GlobalScope.launch { vm.getIdWithUpdate() }
runBlocking {
job.join()
}
MainViewmodel
suspend fun getIdWithUpdate() {
var id = ""
id = notificationAppSessionStorage.getString(
notificationAppSessionStorage.getIncomingKeyValueStorage(),
""
)
if (id != "") {
updateNotificationListItemUseCase.build(id.toInt(), true, false, false)
}
}
}
My proposition:
I have read documentation https://developer.android.com/kotlin/coroutines/coroutines-best-practices
val IODispatcher: CoroutineDispatcher = Dispatchers.IO
val externalScope: CoroutineScope = CoroutineScope(IODispatcher)
suspend {
externalScope.launch(IODispatcher) {
vm.getIdWithUpdate()
}.join()
}
Second option, but here I do not wait until job is done
suspend {
withContext(Dispatchers.IO) {
vm.getIdWithUpdate()
}
}
What do you think about it?
Doesn't it provide to ANR, I also block thread.
You can use lifecycleScope in MainActivity, instead of GlobalScope, to launch a coroutine :
lifecycleScope.launch {
vm.getIdWithUpdate() // calling suspend function
// here suspend function `vm.getIdWithUpdate()` finished execution
// ... do something after suspend function is done
}
To use lifecycleScope add dependency:
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:$2.4.0'
GlobalScope is highly discouraged to use. And there is no need to call job.join(), you can do something in the coroutine builder block after calling vm.getIdWithUpdate(), for example update UI. This coroutine is running using Dispatchers.Main context.

Coroutine Worker gets killed after kill the app

Lets say that i have an activity that starts a worker. inside the worker i do a pseudo suspend proccess and then i print out a result from the database. Here is the code
The activity which starts the worker is
class SplashActivity: BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
val oneTimeRequest = OneTimeWorkRequest.Builder(MyWorker::class.java).setInputData(Data.Builder().apply {
putInt("data", 1)
}.build()).addTag("worktag").build()
WorkManager.getInstance(applicationContext).enqueue(oneTimeRequest)
}
}
The worker is the below
class MyWorker #AssistedInject constructor(
#Assisted private val appContext: Context,
#Assisted private val params: WorkerParameters,
private val serverRepository: ServerRepository
) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
GlobalScope.launch {
for (i in 0..10) {
println("$i")
delay(1000)
}
val servers = serverRepository.getServers()
runOnUiThread {
Toast.makeText(appContext, "${servers.firstOrNull()?.serverAddress}", Toast.LENGTH_SHORT).show()
}
}
return Result.success()
}
}
So the result is that i see in the logcat the system.out with 1,2,3... and then i see a toast messages.
However, when i totally kill the app from the recent while the counter still counts, i never see the toast message.
Why is this happening since i have a GlobalScope coroutine?
And what is the right way to do this??
I was trying to achieve a similar goal. I managed my work by using ForegroundService.
You can find more here
https://developer.android.com/guide/components/foreground-services

Livedata observer are called forever even with removeObserver

I'm facing an issue which drives me crazy.
I have 4 fragments inside an activity.
The logic is: FragA -> FragB -> FragC -> FragD -> FragA -> ...
I'm connected to websockets which post livedata values.
To navigate from FragB to FragC, I'm waiting an event.
The first time, everything works fine, the websockets is recieved, the event is triggered and I'm going to FragC.
But, the second time (after Frag D -> Frag A), if I go back to fragB, the same event is triggered once again. The user doesn't see FragB, and arrives on FragC.
This is the actual behavior but this is not the one I'm expected.
I have do some research and I think it's because the livedata is trigger twice in is normal behavior. And, it can be only dispatch on main thread, so if my fragment goes in the back stack, it will wait for it to be active again.
I have try to removeObserver in the onDestroyView(), it works and the observer is removed, but once the fragment goes again inside onActivityCreated() and I observe the livedata, the observer is instantanetely triggered... I always use "viewLifecycleOwner" as owner.
Is there any way to cancel a liveData execution if I ever go back on an instanciated fragment?
All my frags extends ScopeFragment :
abstract class ScopedFragment : Fragment(), CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
My liveData:
class MyLiveDatas {
private val _myLiveData = MutableLiveData<CustomType>()
val myLiveData: LiveData<CustomType>
get() = _myLiveData
fun customTrigger(webSocketMessage: WebSocketMessage) {
val createdCustomType = CreatedCustomType(webSocketMessage)
_myLiveData.post(createdCustomType)
}
}
My Fragment:
class MyFragment: ScopedFragment(), KodeinAware {
override val kodein by closestKodein()
private val myLiveData: MyLiveDatas by instance()
private val myLiveDataObserver = Observer<CustomType> { customType ->
... my actions
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
myLiveDatas.myLiveData.observe(viewLifecycleOwner, myLiveDataObserver)
}
override fun onDestroyView() {
super.onDestroyView()
myLiveDatas.myLiveData.removeObserver(myLiveDataObserver)
// I've also try removeObservers with viewLifecycleOwner
}
}
Thanks a lot!
You need to use custom live data , in case you want single event
this is my custom mutable live data in one of my project and it is working
class SingleLiveEvent<T> : MediatorLiveData<T>() {
private val observers = ArraySet<ObserverWrapper<in T>>()
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
val wrapper = ObserverWrapper(observer)
observers.add(wrapper)
super.observe(owner, wrapper)
}
#MainThread
override fun removeObserver(observer: Observer<in T>) {
if (observers.remove(observer)) {
super.removeObserver(observer)
return
}
val iterator = observers.iterator()
while (iterator.hasNext()) {
val wrapper = iterator.next()
if (wrapper.observer == observer) {
iterator.remove()
super.removeObserver(wrapper)
break
}
}
}
#MainThread
override fun setValue(t: T?) {
observers.forEach { it.newValue() }
super.setValue(t)
}
private class ObserverWrapper<T>(val observer: Observer<T>) : Observer<T> {
private var pending = false
override fun onChanged(t: T?) {
if (pending) {
pending = false
observer.onChanged(t)
}
}
fun newValue() {
pending = true
}
}
}
LiveData is analogous to a BehaviorRelay, and replays the last value it was told to hold.
LiveData is not LiveEvent, it's not designed for event dispatching.
A regular event bus, a PublishRelay, or something like EventEmitter are better suited for this problem.
Google has devised LiveData<Event<T>> and EventObserver, but if you ever use observe(lifecycleOwner, Observer { instead of observe(lifecycleOwner, EventObserver { it will misbehave, which shows that it's a code smell (LiveData<Event<T>> does not work with Observer, only EventObserver, but its observe method still accepts Observers.)
So personally I'd rather pull in that library EventEmitter I mentioned above, with the LiveEvent helper class.
// ViewModel
private val eventEmitter = EventEmitter<Events>()
val controllerEvents: EventSource<Events> = eventEmitter
// Fragment
viewModel.controllerEvents.observe(viewLifecycleOwner) { event: ControllerEvents ->
when (event) {
is ControllerEvents.NewWordAdded -> showToast("Added ${event.word}")
}.safe()
}
Try to observe the LiveData at onCreate() of the Fragment lifecycle with lifecycle owner as Activity and remove the observer at onDestroy() of the Fragment lifecycle.
Or if that doesn't workout use Event class.
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
This article describes two ways to achieve what you want.
Alternative 1: Wrap your live data in a class that makes sure the value is only observed once.
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
Alternative 2: Use a custom live data class (SingleLiveEvent) that only emits the value once.

Categories

Resources