I am using 'RxJava binding APIs for Android UI widgets' to trigger click events on buttons or textview.
PFB code(Edited) that using to trigger the event
class BookAgentActivity : BaseActivity(), BookAgentView {
#Inject
#field:Named("activity")
lateinit var compositeDisposable: CompositeDisposable
#Inject
lateinit var bookAgentViewModelFactory: BookAgentViewModelFactory
private lateinit var bookAgentViewModel: BookAgentViewModel
private lateinit var cityLocalityJson: CityLocalitiesMO
override fun getLayoutId(): Int {
return R.layout.activity_book_agent
}
override fun initializeDagger() {
IleApplication.getRoomComponent().inject(this)
}
override fun initializeViewModel() {
bookAgentViewModel = ViewModelProviders.of(this, bookAgentViewModelFactory).get(BookAgentViewModel::class.java)
bookAgentViewModel.setView(this)
}
override fun setUpUi() {
gradientStatusBar()
cityLocalityJson = appPreferences.cityLocalitiesJson
compositeDisposable.add(bookAgentCityLocationTV.clicks().observeOn(AndroidSchedulers.mainThread()).subscribe {
startActivity(Intent(this, AreaLocalitiesActivity::class.java)
.putExtra(AppConstants.COMING_FROM_AGENT_KEY, true))
})
compositeDisposable.add(filtersButton.clicks().observeOn(AndroidSchedulers.mainThread()).subscribe {
startActivity(Intent(this, FiltersMainActivity::class.java)
.putExtra(AppConstants.FILTER_TYPE_KEY, AppConstants.AGENT_FILTER))
})
compositeDisposable.add(searchAgentsButton.clicks()
.subscribe { startActivity(Intent(this#BookAgentActivity, SearchAgentActivity::class.java)) })
}
override fun onSuccess(response: Any) {
if (response is AgentsDetailAPIResponse) {
response.let {
val agentDetailsList = it.data
if (agentDetailsList != null && agentDetailsList.size > 0) {
updateAgentPinsOnMap(agentDetailsList)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
compositeDisposable.clear()
}
}
:) Above code works fine for the first time
:( But after coming back from BookAgentActivity (onBackPressed())
Click events are not working for searchAgentsButton as well as for other views too.
Have tried including combinations of other lines of code like below:
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.share()
But none of the above things are working.
Related
I have a specific UseCase where initialize app data. I store every <reference, listener> in a dispatchListeners list to unsubscribe later.
typealias EventListener = Pair<DatabaseReference, ValueEventListener>
class InitAppDataUseCase(
private val subscribeUserUseCase: SubscribeUserUseCase,
private val subscribeNewsUseCase: SubscribeNewsUseCase,
private val subscribeStoriesUseCase: SubscribeStoriesUseCase,
private val subscribeMeetingsUseCase: SubscribeMeetingsUseCase,
private val subscribeCategoriesUseCase: SubscribeCategoriesUseCase,
private val dispatchers: AppDispatchers
): UseCase<Unit, Unit> {
private val dispatchListeners = mutableListOf<EventListener>()
override suspend fun execute(input: Unit) {
init()
}
private fun EventListener.add() = dispatchListeners.add(this)
private suspend fun init() = CoroutineScope(dispatchers.io).launch {
runCatching {
listOf(
async { subscribeUserUseCase.execute().add() },
async { subscribeNewsUseCase.execute().add() },
async { subscribeStoriesUseCase.execute().add() },
async { subscribeMeetingsUseCase.execute().add() },
async { subscribeCategoriesUseCase.execute().add() }
).awaitAll()
}
}
fun clearSubscribed() = CoroutineScope(dispatchers.io).launch {
dispatchListeners.forEach { referenceToListener ->
with(referenceToListener) {
first.removeEventListener(second)
}
}
}
}
But where should I unsubscribe?
When the user remove an account or sign out from my app, I do this in specific ViewModel and redirect him to AuthScreen after this executed.
But what should I do if user just close my app? Is this correct way to unsubscribe in onDestroy() of my MainActivity? I have doubts because clearSubscribed() is a heavy operation. Am I right if the user have a poor internet connection and - this operation couldn't be executed because applicationScope will be dead?
class MainActivity : ComponentActivity() {
private val initAppDataUseCase by inject<InitAppDataUseCase>()
override fun onCreate() {}
override fun onDestroy() {
super.onDestroy()
initAppDataUseCase.clearSubscribed()
}
}
You have to remove the listener according to the life cycle of your activity. Since you're using Kotlin, most likely in an MVVM architecture, I would rather use callbackFlow. There is a very helpful example in the documentation. However, in the case of Firebase, to attach and dettach the listener, please use the following lines of code:
override fun getDataFromRealtimeDatabase() = callbackFlow {
val listener = object: ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
//Do what you need to do with the data.
}
override fun onCancelled(e: DatabaseError) {
Log.d("TAG", "${e?.message}") //Never ignore potential errors!
}
}
yourRef.addValueEventListener(listener) //Attach the listener.
awaitClose {
yourRef.removeEventListener(listener) //Dettach the listener.
}
}
Declaration of variables
private lateinit var wayoJob: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + wayoJob
Function
#InternalCoroutinesApi
private fun startRepeatingJob(timeInterval: Long): Job {
return CoroutineScope(Dispatchers.Default).launch {
while (NonCancellable.isActive) {
Log.i("API", "UPDATE")
delay(timeInterval)
}
}
}
In the Start Button
wayoJob = startRepeatingJob(5000L)
In Button end and onDestroy()
wayoJob.cancel()
When I click the End button it returns an error:
Caused by: kotlin.UninitializedPropertyAccessException: lateinit property wayoJob has not been initialized
Is there any way around this?
I have to do a function that starts executing every few seconds when I press a button and will end when I click another button.
EDIT:
I could try something like this with my own shouldBeActive value and run when I need to, but that's not how it should be
var shouldBeActive = false
#InternalCoroutinesApi
private fun startRepeatingJob(timeInterval: Long): Job {
return CoroutineScope(Dispatchers.Default).launch {
while (NonCancellable.isActive && shouldBeActive) {
Log.i("API", "UPDATE")
delay(timeInterval)
}
}
}
I found a solution.
I have an Activity reload and the object resets
The solution is to insert it like below. This solution ensures that no second object is created in the meantime
companion object {
private lateinit var wayoJob: Job
}
Sample whole class:
class StartActivity : AppCompatActivity(), CoroutineScope {
companion object {
private lateinit var wayoJob: Job
}
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + wayoJob
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_start)
}
#InternalCoroutinesApi
private fun startRepeatingJob(timeInterval: Long): Job {
return CoroutineScope(Dispatchers.Default).launch {
while (NonCancellable.isActive) {
Log.i("API", "UPDATE")
delay(timeInterval)
}
}
}
#InternalCoroutinesApi
fun endBtnOnClick(view: View?) {
wayoJob.cancel()
}
#InternalCoroutinesApi
fun startBtnOnClick(view: View?) {
wayoJob = startRepeatingJob(5000L)
}
override fun onDestroy() {
super.onDestroy()
wayoJob.cancel()
}
}
I have the following ViewModel class -
class VerifyOtpViewModel : ViewModel() {
private var existingUserProfileData: MutableLiveData<TwoVerteUsers.TwoVerteUser>? = null
fun checkInfoForAuthenticatedUser(authorization: String, user: String) {
ProfileNetworking.getUsersProfiles(authorization, GetUserProfilesBodyModel(listOf(user)), object : ProfileNetworking.OnGetUserProfilesListener {
override fun onSuccess(model: TwoVerteUsers) {
existingUserProfileData?.value = model[0]
}
override fun onError(reason: String) {
Log.d("existingProfile", reason)
}
})
}
fun getExistingUserProfileData(): LiveData<TwoVerteUsers.TwoVerteUser>? {
if (existingUserProfileData == null) return null
return existingUserProfileData as LiveData<TwoVerteUsers.TwoVerteUser>
}
}
and the following observer -
private fun initViewModel() {
verifyOtpViewModel = ViewModelProvider(this).get(VerifyOtpViewModel::class.java)
verifyOtpViewModel.getExistingUserProfileData()?.observe(this, Observer {
if (it != null)
Log.d("existingProfile", it.username)
})
}
For some reason the observe is never triggered even after the MutableLiveData object is being given a value
Tried to search for a solution here at stackoverflow but nothing helped
what am I missing?
refactor your code to this, and you should be good to go:
class VerifyOtpViewModel : ViewModel() {
private val _existingUserProfileData = MutableLiveData<TwoVerteUsers.TwoVerteUser>()
val existingUserProfileData: LiveData<TwoVerteUsers.TwoVerteUser>
get() = _existingUserProfileData
fun checkInfoForAuthenticatedUser(authorization: String, user: String) {
ProfileNetworking.getUsersProfiles(
authorization,
GetUserProfilesBodyModel(listOf(user)),
object : ProfileNetworking.OnGetUserProfilesListener {
override fun onSuccess(model: TwoVerteUsers) {
existingUserProfileData.value = model[0]
}
override fun onError(reason: String) {
Log.d("existingProfile", reason)
}
})
}
}
And observing:
verifyOtpViewModel.existingUserProfileData.observe(this, Observer {
.....
})
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.
I have recently seen a weird issue that is acting as a barrier to my project.
Multiple calls to set the live data value does not invoke the observer in the view.
It seems that only the last value that was set actually invokes the Observer in the view.
Here is the code snippet for a review.
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProviders.of(this).get(MainViewModelImpl::class.java)
viewModel.state().observe(this, Observer {
onStateChange(it!!)
})
viewModel.fetchFirstThree()
}
private fun onStateChange(state: MainViewModel.State) {
when (state) {
is One -> {
show(state.data)
}
is Two -> {
show(state.data)
}
is Three -> {
show(state.data)
}
}
}
private fun show(data: String) {
Log.d("Response", data)
}
}
MainViewModel.kt
abstract class MainViewModel : ViewModel() {
sealed class State {
data class One(val data: String) : State()
data class Two(val data: String) : State()
data class Three(val data: String) : State()
}
abstract fun state(): LiveData<State>
abstract fun fetchFirstThree()
}
MainViewModelImpl.kt
class MainViewModelImpl : MainViewModel() {
private val stateLiveData: MediatorLiveData<State> = MediatorLiveData()
override fun state(): LiveData<State> = stateLiveData
override fun fetchFirstThree() {
stateLiveData.value = State.One("One")
stateLiveData.value = State.Two("Two")
stateLiveData.value = State.Three("Three")
}
}
Expected output:
Response: One
Response: Two
Response: Three
Actual Output:
Response: Three
As per the output above, the Observer is not being called for the first two values.
I did some science, re-implementing LiveData and MutableLiveData to log out some data.
Check the source code here.
setValue value=Test1
dispatchingValue mDispatchingValue=false mDispatchInvalidated=false
considerNotify
Returned at !observer.active
setValue value=Test2
dispatchingValue mDispatchingValue=false mDispatchInvalidated=false
considerNotify
Returned at !observer.active
setValue value=Test3
dispatchingValue mDispatchingValue=false mDispatchInvalidated=false
considerNotify
Returned at !observer.active
dispatchingValue mDispatchingValue=false mDispatchInvalidated=false
considerNotify
ITEM: Test3
It looks like the observer hasn't reached an active state when you send the initial values.
private void considerNotify(LifecycleBoundObserver observer) {
// <-- Three times it fails here. This means that your observer wasn't ready for any of them.
if (!observer.active) {
return;
}
Once the observer reaches an active state, it sends the last set value.
void activeStateChanged(boolean newActive) {
if (newActive == active) {
return;
}
active = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
LiveData.this.mActiveCount += active ? 1 : -1;
if (wasInactive && active) {
onActive();
}
if (LiveData.this.mActiveCount == 0 && !active) {
onInactive();
}
if (active) {
// <--- At this point you are getting a call to your observer!
dispatchingValue(this);
}
}
I had such issue too.
To resolve it was created custom MutableLiveData, that contains a queue of posted values and will notify observer for each value.
You can use it the same way as usual MutableLiveData.
open class MultipleLiveEvent<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
private val values: Queue<T> = LinkedList()
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(this::class.java.name, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, { t: T ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
//call next value processing if have such
if (values.isNotEmpty())
pollValue()
}
})
}
override fun postValue(value: T) {
values.add(value)
pollValue()
}
private fun pollValue() {
value = values.poll()
}
#MainThread
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
#Suppress("unused")
#MainThread
fun call() {
value = null
}
}
You could use custom LiveData like this:
class ActiveMutableLiveData<T> : MutableLiveData<T>() {
private val values: Queue<T> = LinkedList()
private var isActive: Boolean = false
override fun onActive() {
isActive = true
while (values.isNotEmpty()) {
setValue(values.poll())
}
}
override fun onInactive() {
isActive = false
}
override fun setValue(value: T) {
if (isActive) {
super.setValue(value)
} else {
values.add(value)
}
}
}
FWIW I had the same problem but solved it like this...
I originally had some code similar to this...
private fun updateMonth(month: Int){
updateMonth.value = UpdateMonth(month, getDaysOfMonth(month))
}
updateMonth(1)
updateMonth(2)
updateMonth(3)
I experienced the same problem as described...
But when I made this simple change....
private fun updateMonth(month: Int) {
CoroutineScope(Dispatchers.Main).launch {
updateMonth.value = UpdateMonth(month, getDaysOfMonth(month))
}
}
Presumably, each updateMonth is going onto a different thread now, so all of the updates are observed.
You should call viewModel.fetchFirstThree() after Activity's onStart() method. for example in onResume() method.
Because in LiveData the Observer is wrapped as a LifecycleBoundObserver. The field mActive set to true after onStart().
class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {
#Override
boolean shouldBeActive() {
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);// return true after onStart()
}
#Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
activeStateChanged(shouldBeActive());// after onStart() change mActive to true
}
}
When the observer notify the change it calls considerNotify, before onStart it will return at !observer.mActive
private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {// called in onCreate() will return here.
return;
}
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
//noinspection unchecked
observer.mObserver.onChanged((T) mData);
}