Pass data from BroadcastReceiver to ViewModel or Repository - android

I'm trying to send the data that I receive in the broadcast(registered in manifest) in the viewmodel or in the repository is not particularly important, I tried to do it both through the live date and through RxJava2, but there is data inside the broadcast class i can see, but it does not come to the viewmodel or repository, thank you.
class MyBroadcastReceiver : BroadcastReceiver() {
private val dataList = MutableLiveData<ArrayList<Words>>()
private var observer: Observable<ArrayList<Words>> = Observable.just(arrayListOf())
#SuppressLint("CheckResult")
override fun onReceive(context: Context?, intent: Intent?) {
val arrayObject =
intent?.extras?.getParcelableArrayList<Words>("KEY") as ArrayList<Words>?
dataList.postValue(arrayObject)
arrayObject.let { dataList.postValue(it) }
arrayObject.let { observer = Observable.just(it) }
observer = Observable.create { emitter: ObservableEmitter<ArrayList<Words>> ->
emitter.onNext(arrayObject!!)
emitter.onComplete()
}
}
fun getDataList() : LiveData<ArrayList<Words>> = dataList
fun getDataListRx() : Observable<ArrayList<Words>> = observer
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
ViewModel class:
class MyViewModel(
private val broadcastReceiver: MyBroadcastReceiver,
private val activity: DaggerActivity) : ViewModel() {
init {
test()
}
#SuppressLint("CheckResult")
private fun test() {
broadcastReceiver.getDataListRx()?.subscribe({
Log.d("WAS_INTENT", "DateRepSuccess")
},{
Log.d("WAS_INTENT", "DateRepError")
})
broadcastReceiver.getDataList().observe(activity, Observer {
Log.d("WAS_INTENT", "DateRepSuccessLiveData")
})
}}

After many attempts, I found out that the liveData works, so maybe someone will need it. RxJava still don`t , I don’t know why yet.

Related

Android Kotlin SingleLiveEvent With Flow

I am using a livedata on viewmodel and flow on repository and data source.
when I tried to connect them to each other and get a data stream as below
that error occurred
Error
java.lang.ClassCastException: androidx.lifecycle.CoroutineLiveData cannot be cast to com.versec.versecko.util.SingleLiveEvent
ViewModel
val singleChat : SingleLiveEvent<ChatRoomEntity> = repository.getChatRooms().asLiveData() as SingleLiveEvent<ChatRoomEntity>
Fragment
viewModel.singleChat.observe(viewLifecycleOwner, Observer {
roomList.add(it)
chatRoomAdapter.changeRooms(roomList)
chatRoomAdapter.notifyDataSetChanged()
})
SingleLiveEvent
class SingleLiveEvent<T> : MutableLiveData<T>() {
companion object {
private val TAG = "SingleLiveEvent"
}
private val pending : AtomicBoolean = AtomicBoolean(false)
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) { Log.w(TAG, "Multiple Observers ,,,")}
super.observe(owner, Observer { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
#MainThread
override fun setValue(value: T?) {
pending.set(true)
super.setValue(value)
}
#MainThread
fun call() {value = null } }
what is the proper way to use both??
From what I understood, the reason you are using SingleLiveData is because you don't want the observer to receive same ChatRoomEntity twice in which case it will be duplicated in the list. A simple solution to this is to maintain the list inside the ViewModel and expose this list to the UI, then you won't have to use workarounds like SingleLiveData.
// ViewModel
private val _chatRoomsLiveData = MutableLiveData(emptyList<ChatRoomEntity>())
val chatRoomsLiveData: LiveData<List<ChatRoomEntity>> = _chatRoomsLiveData
init {
viewModelScope.launch {
repository.getChatRooms().collect {
_chatRoomsLiveData.value = _chatRoomsLiveData.value!! + it
}
}
}
// Fragment
viewModel.chatRoomsLiveData.observe(viewLifecycleOwner, Observer {
chatRoomAdapter.changeRooms(it)
chatRoomAdapter.notifyDataSetChanged()
})

LiveData Value only observes the last added value but using delay makes it working didn't understand why?

In my viewmodel class
class ViewModel(application: Application) : AndroidViewModel(application) {
private val repository: Repository by lazy {
Repository.getInstance(getApplication<BaseApplication>().retrofitFactory)
}
private var _liveData = MutableLiveData<ItemState>()
val liveData: LiveData<ItemState> = _liveData
init {
fetchData()
}
private fun fetchData() {
repository.getLiveData().observeForever(liveDataObserver)
}
override fun onCleared() {
super.onCleared()
repository.getLiveData().removeObserver(liveDataObserver)
}
private val liveDataObserver = Observer<User> {
if (it != null) {
setData(it)
}
}
private fun setData(it: User) =viewModelScope.launch {
val list1 = mutableListOf<something1>()
val list2 = mutableListOf<something2>()
list1.add(it.data)
list2.add(it.data)
}
_liveData.value = ItemState.State1(list1)
delay(1)
_liveData.value = ItemState.State2(list2)
}
The ItemState is a sealed class with two data members
sealed class ItemState {
data class State1(val list: List<something1>) : ItemState()
data class State2(val list: List<something2>) : ItemState()
}
Activity Observer Code
viewModel.liveData.observe(this, Observer {
loadDataIntoUi(it)
})
private fun loadDataIntoUi(data: ItemState) {
when (data) {
is ItemState.State1 -> adaptr1.addItems(data.list)
is ItemState.State2 -> adaptr2.addItems(data.list)
}
Now if i don't use delay in my viewModel here like above the livedata first value that is Office doesn't get observed but it works fine with delay
I have done a lot of research didn't understand why this happening also I have many alternate solutions to this but my question is why delay make's it working

Is there a way to achieve this rx flow in Kotlin with coroutines/Flow/Channels?

I am trying out Kotlin Coroutines and Flow for the first time and I am trying to reproduce a certain flow I use on Android with RxJava with an MVI-ish approach, but I am having difficulties getting it right and I am essentially stuck at this point.
The RxJava app looks essentially like this:
MainActivityView.kt
object MainActivityView {
sealed class Event {
object OnViewInitialised : Event()
}
data class State(
val renderEvent: RenderEvent = RenderEvent.None
)
sealed class RenderEvent {
object None : RenderEvent()
class DisplayText(val text: String) : RenderEvent()
}
}
MainActivity.kt
MainActivity has an instance of a PublishSubject with a Event type. Ie MainActivityView.Event.OnViewInitialised, MainActivityView.Event.OnError etc. The initial Event is sent in onCreate() via the subjects's .onNext(Event) call.
#MainActivityScope
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var subscriptions: CompositeDisposable
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit var onViewInitialisedSubject: PublishSubject<MainActivityView.Event.OnViewInitialised>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupEvents()
}
override fun onDestroy() {
super.onDestroy()
subscriptions.clear()
}
private fun setupEvents() {
if (subscriptions.size() == 0) {
Observable.mergeArray(
onViewInitialisedSubject
.toFlowable(BackpressureStrategy.BUFFER)
.toObservable()
).observeOn(
Schedulers.io()
).compose(
viewModel()
).observeOn(
AndroidSchedulers.mainThread()
).subscribe(
::render
).addTo(
subscriptions
)
onViewInitialisedSubject
.onNext(
MainActivityView
.Event
.OnViewInitialised
)
}
}
private fun render(state: MainActivityView.State) {
when (state.renderEvent) {
MainActivityView.RenderEvent.None -> Unit
is MainActivityView.RenderEvent.DisplayText -> {
mainActivityTextField.text = state.renderEvent.text
}
}
}
}
MainActivityViewModel.kt
These Event's are then picked up by a MainActivityViewModel class which is invoked by .compose(viewModel()) which then transform the received Event into a sort of a new State via ObservableTransformer<Event, State>. The viewmodel returns a new state with a renderEvent in it, which can then be acted upon in the MainActivity again via render(state: MainActivityView.State)function.
#MainActivityScope
class MainActivityViewModel #Inject constructor(
private var state: MainActivityView.State
) {
operator fun invoke(): ObservableTransformer<MainActivityView.Event, MainActivityView.State> = onEvent
private val onEvent = ObservableTransformer<MainActivityView.Event,
MainActivityView.State> { upstream: Observable<MainActivityView.Event> ->
upstream.publish { shared: Observable<MainActivityView.Event> ->
Observable.mergeArray(
shared.ofType(MainActivityView.Event.OnViewInitialised::class.java)
).compose(
eventToViewState
)
}
}
private val eventToViewState = ObservableTransformer<MainActivityView.Event, MainActivityView.State> { upstream ->
upstream.flatMap { event ->
when (event) {
MainActivityView.Event.OnViewInitialised -> onViewInitialisedEvent()
}
}
}
private fun onViewInitialisedEvent(): Observable<MainActivityView.State> {
val renderEvent = MainActivityView.RenderEvent.DisplayText(text = "hello world")
state = state.copy(renderEvent = renderEvent)
return state.asObservable()
}
}
Could I achieve sort of the same flow with coroutines/Flow/Channels? Possibly a bit simplified even?
EDIT:
I have since found a solution that works for me, I haven't found any issues thus far. However this solution uses ConflatedBroadcastChannel<T> which eventually will be deprecated, it will likely be possible to replace it with (at the time of writing) not yet released SharedFlow api (more on that here.
The way it works is that the Activity and viewmodel shares
a ConflatedBroadcastChannel<MainActivity.Event> which is used to send or offer events from the Activity (or an adapter). The viewmodel reduce the event to a new State which is then emitted. The Activity is collecting on the Flow<State> returned by viewModel.invoke(), and ultimately renders the emitted State.
MainActivityView.kt
object MainActivityView {
sealed class Event {
object OnViewInitialised : Event()
data class OnButtonClicked(val idOfItemClicked: Int) : Event()
}
data class State(
val renderEvent: RenderEvent = RenderEvent.Idle
)
sealed class RenderEvent {
object Idle : RenderEvent()
data class DisplayText(val text: String) : RenderEvent()
}
}
MainActivity.kt
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit eventChannel: ConflatedBroadcastChannel<MainActivityView.Event>
private var isInitialised: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
init()
}
private fun init() {
if (!isInitialised) {
lifecycleScope.launch {
viewModel()
.flowOn(
Dispatchers.IO
).collect(::render)
}
eventChannel
.offer(
MainActivityView.Event.OnViewInitialised
)
isInitialised = true
}
}
private suspend fun render(state: MainActivityView.State): Unit =
when (state.renderEvent) {
MainActivityView.RenderEvent.Idle -> Unit
is MainActivityView.RenderEvent.DisplayText ->
renderDisplayText(text = state.renderEvent.text)
}
private val renderDisplayText(text: String) {
// render text
}
}
MainActivityViewModel.kt
class MainActivityViewModel constructor(
private var state: MainActivityView.State = MainActivityView.State(),
private val eventChannel: ConflatedBroadcastChannel<MainActivityView.Event>,
) {
suspend fun invoke(): Flow<MainActivityView.State> =
eventChannel
.asFlow()
.flatMapLatest { event: MainActivityView.Event ->
reduce(event)
}
private fun reduce(event: MainActivityView.Event): Flow<MainActivityView.State> =
when (event) {
MainActivityView.Event.OnViewInitialised -> onViewInitialisedEvent()
MainActivityView.Event.OnButtonClicked -> onButtonClickedEvent(event.idOfItemClicked)
}
private fun onViewInitialisedEvent(): Flow<MainActivityView.State> = flow
val renderEvent = MainActivityView.RenderEvent.DisplayText(text = "hello world")
state = state.copy(renderEvent = renderEvent)
emit(state)
}
private fun onButtonClickedEvent(idOfItemClicked: Int): Flow<MainActivityView.State> = flow
// do something to handle click
println("item clicked: $idOfItemClicked")
emit(state)
}
}
Similiar questions:
publishsubject-with-kotlin-coroutines-flow
Your MainActivity can look something like this.
#MainActivityScope
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var subscriptions: CompositeDisposable
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit var onViewInitialisedChannel: BroadcastChannel<MainActivityView.Event.OnViewInitialised>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupEvents()
}
override fun onDestroy() {
super.onDestroy()
subscriptions.clear()
}
private fun setupEvents() {
if (subscriptions.size() == 0) {
onViewInitialisedChannel.asFlow()
.buffer()
.flowOn(Dispatchers.IO)
.onEach(::render)
.launchIn(GlobalScope)
onViewInitialisedChannel
.offer(
MainActivityView
.Event
.OnViewInitialised
)
}
}
private fun render(state: MainActivityView.State) {
when (state.renderEvent) {
MainActivityView.RenderEvent.None -> Unit
is MainActivityView.RenderEvent.DisplayText -> {
mainActivityTextField.text = state.renderEvent.text
}
}
}
}
I think what you're looking for is the Flow version of compose and ObservableTransformer and as far as I can tell there isn't one. What you can use instead is the let operator and do something like this:
MainActivity:
yourFlow
.let(viewModel::invoke)
.onEach(::render)
.launchIn(lifecycleScope) // or viewLifecycleOwner.lifecycleScope if you're in a fragment
ViewModel:
operator fun invoke(viewEventFlow: Flow<Event>): Flow<State> = viewEventFlow.flatMapLatest { event ->
when (event) {
Event.OnViewInitialised -> flowOf(onViewInitialisedEvent())
}
}
As far as sharing a flow I would watch these issues:
https://github.com/Kotlin/kotlinx.coroutines/issues/2034
https://github.com/Kotlin/kotlinx.coroutines/issues/2047
Dominic's answer might work for replacing the publish subjects but I think the coroutines team is moving away from BroadcastChannel and intends to deprecate it in the near future.
kotlinx-coroutines-core provides a transform function.
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/transform.html
it isn't quite the same as what we are used to in RxJava but should be usable for achieving the same result.

LiveData is observed multiple times inside onClickListener in Android

I have a repository setup like this
class ServerTimeRepo #Inject constructor(private val retrofit: Retrofit){
var liveDataTime = MutableLiveData<TimeResponse>()
fun getServerTime(): LiveData<TimeResponse> {
val serverTimeService:ServerTimeService = retrofit.create(ServerTimeService::class.java)
val obs = serverTimeService.getServerTime()
obs.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).unsubscribeOn(Schedulers.io())
.subscribe(object : Observer<Response<TimeResponse>> {
override fun onComplete() {
}
override fun onSubscribe(d: Disposable) {
}
override fun onNext(t: Response<TimeResponse>) {
val gson = Gson()
val json: String?
val code = t.code()
val cs = code.toString()
if (!cs.equals("200")) {
json = t.errorBody()!!.string()
val userError = gson.fromJson(json, Error::class.java)
} else {
liveDataTime.value = t.body()
}
}
override fun onError(e: Throwable) {
}
})
return liveDataTime
}
}
Then I have a viewmodel calling this repo like this
class ServerTimeViewModel #Inject constructor(private val serverTimeRepo: ServerTimeRepo):ViewModel() {
fun getServerTime(): LiveData<TimeResponse> {
return serverTimeRepo.getServerTime()
}
}
Then I have an activity where I have an onClickListener where I am observing the livedata, like this
tvPWStart.setOnClickListener {
val stlv= serverTimeViewModel.getServerTime()
stlv.observe(this#HomeScreenActivity, Observer {
//this is getting called multiple times??
})
}
I don't know what's wrong in this. Can anyone point me in the right direction? Thanks.
Issue is that every time your ClickListener gets fired, you observe LiveData again and again. So, you can solve that problem by following solution :
Take a MutableLiveData object inside your ViewModel privately & Observe it as LiveData.
class ServerTimeViewModel #Inject constructor(private val serverTimeRepo: ServerTimeRepo):ViewModel() {
private val serverTimeData = MutableLiveData<TimeResponse>() // We make private variable so that UI/View can't modify directly
fun getServerTime() {
serverTimeData.value = serverTimeRepo.getServerTime().value // Rather than returning LiveData, we set value to our local MutableLiveData
}
fun observeServerTime(): LiveData<TimeResponse> {
return serverTimeData //Here we expose our MutableLiveData as LiveData to avoid modification from UI/View
}
}
Now, we observe this LiveData directly outside of ClickListener and we just call API method from button click like below :
//Assuming that this code is inside onCreate() of your Activity/Fragment
//first we observe our LiveData
serverTimeViewModel.observeServerTime().observe(this#HomeScreenActivity, Observer {
//In such case, we won't observe multiple LiveData but one
})
//Then during our ClickListener, we just do API method call without any callback.
tvPWStart.setOnClickListener {
serverTimeViewModel.getServerTime()
}

Observer onChanged never called

FooActivity.kt:
class FooActivity : AppCompatActivity(), LifecycleRegistryOwner {
override fun getLifecycle(): LifecycleRegistry {
return LifecycleRegistry(this)
}
..
// <-- here mViewModel is null
mViewModel.getBar().observe(this, Observer<List<String>> {
override fun onChanged(bar: List<String>) {
// Never triggered
}
})
mViewModel.init()
// <-- here mViewModel has changed
}
The mViewModel is confirmed to change. However the observer's onChanged is never called.
Question: why doesn't it work?
Edit: FooViewModel.kt:
class FooViewModel(application: Application) : AndroidViewModel(application) {
val baz: BazPagerAdapter? = null
..
fun init(fm: FragmentManager) {
mBar = listOf("1", "2", "3")
}
..
fun getBar(): List<String> = mBar
..
fun setBaz(pager: ViewPager, periods: List<BazFragment>) {
pager.adapter = BazPagerAdapter(mFragmentManager!!, periods)
}
}
Edit2:
For got to mentiond, getBar already returns LiveData
fun getBar(): LiveData<List<String>> = mBar
And the onChange still wouldn't trigger.
Edit3:
class FooViewModel(application: Application) : AndroidViewModel(application) {
private var mBar: MutableLiveData<List<String>>? = null
..
fun init(fragmentManager: FragmentManager) {
..
if (mBar == null) {
mBar = MutableLiveData<List<String>>()
}
mBar?.value = periods
}
..
fun getBar(): LiveData<List<String>>? = mBar
There is no observe method for type List.
The ViewModel has nothing to do with observing either, it is there mainly to have state that persists through configuration changes.
For observable data you want (Mutable)LiveData objects. These are lifecycle aware and manage observers for their data.
Please see the code examples here:
public class MyViewModel : ViewModel() {
private val mBar = MutableLiveData<List<String>>()
fun getBar(): LiveData<List<String>> = mBar
fun init() {
mBar.setValue(listOf("1", "2", "3"))
}
}
Ok, so I have managed to find the answer.
If you are going to extend AppCompatActivity and you want to use LiveData, you will have to implement the LifecycleRegistryOwner interface and its only method - getLifecycle.
The problem was that:
override fun getLifecycle(): LifecycleRegistry {
return LifecycleRegistry(this)
}
Had to be:
val mLifecycleRegistry = LifecycleRegistry(this)
..
override fun getLifecycle(): LifecycleRegistry {
return mLifecycleRegistry
}
So the problem is when you call init method, it reassigns new instance of LiveData into mBar. And you have assigned observer to previous instance of mBar because you are calling init method after :
mViewModel.getBar().observe(this, Observer<List<String>> {
override fun onChanged(bar: List<String>) {
// Never triggered
}
})
To solve the problem just initialise mBar with MutableLiveData and then change its value (Do not re-assign mBar with another instance).
Check following code:
class FooViewModel(application: Application) : AndroidViewModel(application) {
private var mBar: MutableLiveData<List<String>> = MutableLiveData()
fun init(fragmentManager: FragmentManager) {
mBar.value = periods // Changing value only, not new instance
}
fun getBar(): LiveData<List<String>> = mBar
}

Categories

Resources