Coroutine Job operation at the push of a button - android

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()
}
}

Related

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.

RxBinding 'clicks()' method not triggering again when coming back from another activity

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.

Error "Can't create ViewModelProvider for detached fragment" when i called method from Presenter

I have a fragment with updateToolbar() function. When i try to get my ViewModel, calles UserViewModel on this method i get error:
Can't create ViewModelProvider for detached fragment
When i try to get UserViewModel inside onViewCreated(), all works fine. Why it happens? I call updateToolbar() after onCreateView() and I'm not create any fragment transactions before function called.
I'm started to learn Clean Architecture, and intuitively i think the reason of error can be on it, so i add this code too. I think problem about presenter, but i can't understand where exactly.
PacksFragment:
class PacksFragment : BaseCompatFragment() {
#Inject
lateinit var presenter: PacksFragmentPresenter
private var userViewModel: UserViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
userViewModel = ViewModelProviders.of(this).get(UserViewModel::class.java)
super.onCreate(savedInstanceState)
}
override fun onCreateView(
...
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
LibApp.get().injector.inject(this)
...
presenter.openNewPack(packId)
}
fun updateToolbar() {
Timber.e((userViewModel == null).toString())
Timber.e(userViewModel?.getData()?.value?.coins.toString())
}
}
PacksFragmentPresenter:
class PacksFragmentPresenter #Inject constructor(
private val packsFragment: PacksFragment,
private val getCoinsFromUserCase: GetCoinsFromUserCase
) {
fun openNewPack(packId: Int) {
if (getCoinsFromUserCase.getCoinsFromUser()){
packsFragment.updateToolbar()
}
}
}
GetCoinsFromUserCase:
class GetCoinsFromUserCase {
fun getCoinsFromUser(): Boolean {
val userViewModel = UserViewModel()
userViewModel.takeCoins(10)
return true
}
}
userViewModel:
class UserViewModel : ViewModel(), UserApi {
private val data = MutableLiveData<User>()
fun setData(user: User) {
//Logged fine
Timber.e("User setted")
data.value = user
}
fun getData(): LiveData<User> {
if (data.value == null) {
val user = User()
user.coins = 200
data.value = user
}
//Logged fine, "false"
Timber.e("User getted")
Timber.e("Is user == null? %s", (data.value == null).toString())
return data
}
override fun takeCoins(value: Int) {
//Specially commented it
// getCoins(value)
}
}
UPD:
I make some changes to prevent crush - make userViewModel nullable(update PacksFragment code on the top).
But userViewModel is always null when i call updateToolbar(). Main thing fragment not removed/deleted/invisible/..., it's active fragment with button which called updateToolbar() function.

Live data observer not triggered after room db query

I have a LiveData object which i'm observing within a fragment within an activity. The activity listens to broadcasts from the system via 2 broadcast receivers. The data is queried from room according to the current date. So the receivers detect if the date was changed and if so, go and refresh the query.
Problem is after i change the date in the settings and come back to the app, the observer isn't triggered. If i go to another fragment and return the data will change, but not from the
Now for the code:
Activity:
private lateinit var timeReceiver: MyTimeReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
timeReceiver = MyTimeReceiver()
registerReceiver(timeReceiver, filter)
registerReceiver(refreshReceiver, IntentFilter("refresh_data"))
mainFragment = MainFragment.newInstance()
}
private val refreshReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "refresh_data") {
Timber.e("time changed, need to refresh data")
mainFragment.refreshData()
}
}
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(timeReceiver)
unregisterReceiver(refreshReceiver)
}
Fragment:
private var counter: Int = 0
private lateinit var viewModel: MainFragmentViewModel
private lateinit var date: String
companion object {
fun newInstance(): MainFragment {
return MainFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_main, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
plus_btn.setOnClickListener { addCup() }
minus_btn.setOnClickListener { decreaseCup() }
viewModel = ViewModelProviders.of(this).get(MainFragmentViewModel::class.java)
viewModel.dataList.observe(viewLifecycleOwner, Observer {
it.let { it1 ->
counter = it1.size
cup_counter.text = "$counter"
setGoalVisibility()
}
})
}
override fun onResume() {
super.onResume()
viewModel.checkCups(setDate())
}
private fun decreaseCup() {
viewModel.deleteCup(counter, date)
}
private fun addCup() {
Timber.e(date)
viewModel.insertCup(
WaterCupEntity(
System.currentTimeMillis(),
++counter,
date
)
)
}
fun refreshData() {
viewModel.checkCups(setDate())
}
private fun setDate(): String {
val dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.US)
date = dateFormat.format(Calendar.getInstance().time)
return date
}
ViewModel:
class MainFragmentViewModel(application: Application) : AndroidViewModel(application) {
private var dao: WaterCupDao
var dataList: LiveData<List<WaterCupEntity>>
private var job = Job()
private val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private val scope = CoroutineScope(coroutineContext)
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.US)
var date: String
init {
dao = MyCupsDb.getInstance(application, scope).getDao()
date = dateFormat.format(Calendar.getInstance().time)
Timber.e("getting all cups for date: %s", date)
dataList = dao.getAllCupsWithSameDate(date)
}
fun checkCups(date :String) {
dataList = dao.getAllCupsWithSameDate(date)
}
fun insertCup(cup: WaterCupEntity) = runBlocking {
scope.launch(Dispatchers.IO) {
dao.insert(cup)
}
}
fun deleteCup(number: Int, date: String) =
scope.launch(Dispatchers.IO) {
dao.deleteCupForDate(number, date)
}
fun deleteToday(date :String) {
scope.launch (Dispatchers.IO){
dao.deleteAllCupsForDate(date)
}
}
fun deleteAllCups() {
scope.launch (Dispatchers.IO){
dao.deleteAllCups()
}
}
}
Room db :
#Database(entities = [WaterCupEntity::class], version = 1)
abstract class MyCupsDb : RoomDatabase() {
abstract fun getDao(): WaterCupDao
companion object {
#Volatile
private var instance: MyCupsDb? = null
fun getInstance(context: Context, scope:CoroutineScope): MyCupsDb {
return instance ?: synchronized(this) {
instance ?: buildDb(context,scope).also { instance = it }
}
}
private fun buildDb(context: Context,scope:CoroutineScope): MyCupsDb {
return Room.databaseBuilder(context, MyCupsDb::class.java, "myDb")
.addCallback(WordDatabaseCallback(scope))
.build()
}
}
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onOpen(db)
instance?.let { database ->
scope.launch(Dispatchers.IO) {
populateDatabase(database.getDao())
}
}
}
fun populateDatabase(dao: WaterCupDao) {
var word = WaterCupEntity(System.currentTimeMillis(),1,"11.01.2019")
dao.insert(word)
word = WaterCupEntity(System.currentTimeMillis(),2,"11.01.2019")
dao.insert(word)
}
}
}
MyTimeReceiver:
class MyTimeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action){
Intent.ACTION_TIMEZONE_CHANGED,
Intent.ACTION_DATE_CHANGED,
Intent.ACTION_TIME_CHANGED,
Intent.ACTION_TIME_TICK -> context.sendBroadcast(Intent("refresh_data"))
}
}
}
So mainly the use case was to trigger the LiveData query after some event - in this case an intent from a BroadcastReceiver - so it is refreshed.
I wanted to put new params for the query and then make the query work.
What i didn't realize is this is basically against the design of LiveData. LiveData is data that you subscribe to, a client that subscribes to that data doesn't affect it, it merely listens to its changes.
So the problem here is with design.
To get a new query you usually need either a new data (so your observe will trigger) or resubscribe to the LiveData - the harder and more complicated approach since then you need to manage the subscriptions so you don't leak.
I chose to get a new data via using an insert once i get an intent. If anyone so wishes i can post the fixed code for this.

How do I run coroutines as blocking for unit testing?

I've begun writing unit tests for my MVP Android project, but my tests dependent on coroutines intermittently fail (through logging and debugging I've confirmed verify sometimes occurs early, adding delay fixes this of course)
I've tried wrapping with runBlocking and I've discovered Dispatchers.setMain(mainThreadSurrogate) from org.jetbrains.kotlinx:kotlinx-coroutines-test, but trying so many combinations hasn't yielded any success so far.
abstract class CoroutinePresenter : Presenter, CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onCreate() {
super.onCreate()
job = Job()
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
class MainPresenter #Inject constructor(private val getInfoUsecase: GetInfoUsecase) : CoroutinePresenter() {
lateinit var view: View
fun inject(view: View) {
this.view = view
}
override fun onResume() {
super.onResume()
refreshInfo()
}
fun refreshInfo() = launch {
view.showLoading()
view.showInfo(getInfoUsecase.getInfo())
view.hideLoading()
}
interface View {
fun showLoading()
fun hideLoading()
fun showInfo(info: Info)
}
}
class MainPresenterTest {
private val mainThreadSurrogate = newSingleThreadContext("Mocked UI thread")
private lateinit var presenter: MainPresenter
private lateinit var view: MainPresenter.View
val expectedInfo = Info()
#Before
fun setUp() {
Dispatchers.setMain(mainThreadSurrogate)
view = mock()
val mockInfoUseCase = mock<GetInfoUsecase> {
on { runBlocking { getInfo() } } doReturn expectedInfo
}
presenter = MainPresenter(mockInfoUseCase)
presenter.inject(view)
presenter.onCreate()
}
#Test
fun onResume_RefreshView() {
presenter.onResume()
verify(view).showLoading()
verify(view).showInfo(expectedInfo)
verify(view).hideLoading()
}
#After
fun tearDown() {
Dispatchers.resetMain()
mainThreadSurrogate.close()
}
}
I believe the runBlocking blocks should be forcing all child coroutineScopes to run on the same thread, forcing them to complete before moving on to verification.
In CoroutinePresenter class you are using Dispatchers.Main. You should be able to change it in the tests. Try to do the following:
Add uiContext: CoroutineContext parameter to your presenters' constructor:
abstract class CoroutinePresenter(private val uiContext: CoroutineContext = Dispatchers.Main) : CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = uiContext + job
//...
}
class MainPresenter(private val getInfoUsecase: GetInfoUsecase,
private val uiContext: CoroutineContext = Dispatchers.Main
) : CoroutinePresenter(uiContext) { ... }
Change MainPresenterTest class to inject another CoroutineContext:
class MainPresenterTest {
private lateinit var presenter: MainPresenter
#Mock
private lateinit var view: MainPresenter.View
#Mock
private lateinit var mockInfoUseCase: GetInfoUsecase
val expectedInfo = Info()
#Before
fun setUp() {
// Mockito has a very convenient way to inject mocks by using the #Mock annotation. To
// inject the mocks in the test the initMocks method needs to be called.
MockitoAnnotations.initMocks(this)
presenter = MainPresenter(mockInfoUseCase, Dispatchers.Unconfined) // here another CoroutineContext is injected
presenter.inject(view)
presenter.onCreate()
}
#Test
fun onResume_RefreshView() = runBlocking {
Mockito.`when`(mockInfoUseCase.getInfo()).thenReturn(expectedInfo)
presenter.onResume()
verify(view).showLoading()
verify(view).showInfo(expectedInfo)
verify(view).hideLoading()
}
}
#Sergey's answer caused me to read further into Dispatchers.Unconfined and I realised that I was not using Dispatchers.setMain() to its fullest extent. At the time of writing, note this solution is experimental.
By removing any mention of:
private val mainThreadSurrogate = newSingleThreadContext("Mocked UI thread")
and instead setting the main dispatcher to
Dispatchers.setMain(Dispatchers.Unconfined)
This has the same result.
A less idiomatic method but one that may assist anyone as a stop-gap solution is to block until all child coroutine jobs have completed (credit: https://stackoverflow.com/a/53335224/4101825):
this.coroutineContext[Job]!!.children.forEach { it.join() }

Categories

Resources