I use this code for eventBus in kotlin, but it triggers twice. I don't know why this happens
object EventBus {
val bus: BroadcastChannel<Any> = BroadcastChannel<Any>(1)
private val parentJob = Job()
private val coroutineContext: CoroutineContext
get() = parentJob + Dispatchers.Default
private val scope = CoroutineScope(coroutineContext)
fun send(o: Any) {
scope.launch {
bus.send(o)
}
}
inline fun <reified T> asChannel(): ReceiveChannel<T> {
return bus.openSubscription().filter { it is T }.map { it as T }
}
}
and use it in this way
EventBus.send(NetEvent(false))
and listen t it as below (this code runs twice)
var subscription = EventBus.asChannel<NetEvent>()
var s = scope.launch {
subscription.consumeEach { event ->
Timber.i("NetEvent ${event.isConected}")
currentNet = event.isConected
}
}
I would prefer MutableSharedFlow:
class EventBus {
private val events = MutableSharedFlow<Any>()
suspend fun dispatch(event: Any) {
events.emit(event)
}
suspend fun on(coroutineScope: CoroutineScope, handler: suspend (Any) -> Unit) =
coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
events.asSharedFlow().collect { event -> handler(event) }
}
}
class EventBusTest {
#Test
fun testEventBus() {
val events = ConcurrentLinkedDeque<String>()
runBlocking {
val eventBus = EventBus()
val job1 = eventBus.on(this) { events.add("1: $it") }
eventBus.dispatch("a")
val job2 = eventBus.on(this) { events.add("2: $it") }
eventBus.dispatch("b")
job1.cancelAndJoin()
eventBus.dispatch("c")
job2.cancelAndJoin()
}
assertEquals(listOf("1: a", "1: b", "2: b", "2: c"), events.toList())
}
}
Related
So, I have this ViewModel class
#HiltViewModel
class HomeViewModel #Inject constructor(private val starWarsRepository: StarWarsRepository) : ViewModel() {
/**
* Two-Way binding variable
*/
val searchQuery: MutableLiveData<String> = MutableLiveData()
val characters = searchQuery.switchMap { query ->
if (query.isNotBlank() && query.isNotEmpty()) {
characterPager.liveData.cachedIn(viewModelScope)
} else {
MutableLiveData()
}
}
fun retrySearch() {
searchQuery.postValue(searchQuery.value)
}
private val characterPager by lazy {
val searchPagingConfig = PagingConfig(pageSize = 20, maxSize = 100, enablePlaceholders = false)
Pager(config = searchPagingConfig) {
CharacterSearchPagingSource(starWarsRepository, searchQuery.value ?: String())
}
}
}
and here it's counter TestingClass
#ExperimentalCoroutinesApi
#RunWith(JUnit4::class)
class HomeViewModelTest {
#get:Rule
val instantTaskExecutionRule = InstantTaskExecutorRule()
private lateinit var homeViewModel: HomeViewModel
private val starWarsAPI = mock(StarWarsAPI::class.java)
private val testDispatcher = UnconfinedTestDispatcher()
#Before
fun setup() {
Dispatchers.setMain(testDispatcher)
homeViewModel = HomeViewModel(StarWarsRepositoryImpl(starWarsAPI, testDispatcher))
}
#AfterTest
fun tearDown() {
Dispatchers.resetMain()
}
#Test
fun `live data should emit success with result when searching for a character`() = runTest {
val character = CharacterEntity("", "", "", "172", "", listOf(), listOf())
`when`(starWarsAPI.searchCharacters("a", null)).thenReturn(CharacterSearchResultEntity(listOf(character, character), null, null))
homeViewModel.searchQuery.value = "a"
val result = homeViewModel.characters.getOrAwaitValue()
assertEquals(PagingSource.LoadResult.Page(listOf(), null, null), result)
}
}
Here is the repository
class StarWarsRepositoryImpl(private val starWarsAPI: StarWarsAPI, private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO) : StarWarsRepository {
override suspend fun searchCharacters(query: String, page: Int?) = withContext(coroutineDispatcher) {
starWarsAPI.searchCharacters(query, page)
}
}
I'm using this extension function from a google sample.
fun <T> LiveData<T>.getOrAwaitValue(time: Long = 2, timeUnit: TimeUnit = TimeUnit.SECONDS, afterObserve: () -> Unit = {}): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this#getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
#Suppress("UNCHECKED_CAST")
return data as T
}
The problem is I'm getting an exception from the getOrAwaitValue that LiveData value was never set.?
I am almost new to android testing and following the official docs and Udacity course for learning purposes.
Coming to the issue I want to check when the task is completed or incompleted to be displayed properly or not, for this I wrote a few tests. Here I got the exception that toast can not be displayed on a thread that has not called Looper.prepare.
When I comment out the toast msg live data updating line of code then all tests work fine and pass successfully. I am new to android testing and searched out a lot but did not get any info to solve this issue. Any help would be much appreciated. A little bit of explanation will be much more helpful if provided.
Below is my test class source code along with ViewModel, FakeRepository, and fragment source code.
Test Class.
#ExperimentalCoroutinesApi
#MediumTest
#RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
#get:Rule
var mainCoroutineRule = MainCoroutineRule()
#get:Rule
val rule = InstantTaskExecutorRule()
private lateinit var tasksRepository: FakeTasksRepository
#Before
fun setUp() {
tasksRepository = FakeTasksRepository()
ServiceLocator.taskRepositories = tasksRepository
}
#Test
fun addNewTask_addNewTaskToDatabase() = mainCoroutineRule.runBlockingTest {
val newTask = Task(id = "1", userId = 0, title = "Hello AndroidX World",false)
tasksRepository.addTasks(newTask)
val task = tasksRepository.getTask(newTask.id)
assertEquals(newTask.id,(task as Result.Success).data.id)
}
#Test
fun activeTaskDetails_DisplayedInUi() = mainCoroutineRule.runBlockingTest {
val newTask = Task(id = "2", userId = 0, title = "Hello AndroidX World",false)
tasksRepository.addTasks(newTask)
val bundle = TaskDetailFragmentArgs(newTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.Theme_ToDoWithTDD)
onView(withId(R.id.title_text)).check(matches(isDisplayed()))
onView(withId(R.id.title_text)).check(matches(withText("Hello AndroidX World")))
onView(withId(R.id.complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.complete_checkbox)).check(matches(isNotChecked()))
}
#Test
fun completedTaskDetails_DisplayedInUI() = mainCoroutineRule.runBlockingTest {
val newTask = Task(id = "2", userId = 0, title = "Hello AndroidX World",true)
tasksRepository.addTasks(newTask)
val bundle = TaskDetailFragmentArgs(newTask.id).toBundle()
launchFragmentInContainer <TaskDetailFragment>(bundle,R.style.Theme_ToDoWithTDD)
onView(withId(R.id.title_text)).check(matches(isDisplayed()))
onView(withId(R.id.title_text)).check(matches(withText("Hello AndroidX World")))
onView(withId(R.id.complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.complete_checkbox)).check(matches(isChecked()))
}
#After
fun tearDown() = mainCoroutineRule.runBlockingTest {
ServiceLocator.resetRepository()
}
}
FakeRepository class.
class FakeTasksRepository: TasksRepository {
var tasksServiceData: LinkedHashMap<String,Task> = LinkedHashMap()
private val observableTasks: MutableLiveData<Result<List<Task>>> = MutableLiveData()
private var shouldReturnError: Boolean = false
fun setReturnError(value: Boolean) {
shouldReturnError = value
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
return observableTasks
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
runBlocking { fetchAllToDoTasks() }
return observableTasks.map { tasks ->
when(tasks) {
is Result.Loading -> Result.Loading
is Result.Error -> Result.Error(tasks.exception)
is Result.Success -> {
val task = tasks.data.firstOrNull() { it.id == taskId }
?: return#map Result.Error(Exception("Not found"))
Result.Success(task)
}
}
}
}
override suspend fun completeTask(id: String) {
tasksServiceData[id]?.completed = true
}
override suspend fun completeTask(task: Task) {
val compTask = task.copy(completed = true)
tasksServiceData[task.id] = compTask
fetchAllToDoTasks()
}
override suspend fun activateTask(id: String) {
tasksServiceData[id]?.completed = false
}
override suspend fun activateTask(task: Task) {
val activeTask = task.copy(completed = false)
tasksServiceData[task.id] = activeTask
fetchAllToDoTasks()
}
override suspend fun getTask(taskId: String): Result<Task> {
if (shouldReturnError) return Result.Error(Exception("Test Exception"))
tasksServiceData[taskId]?.let {
return Result.Success(it)
}
return Result.Error(Exception("Could not find task"))
}
override suspend fun getTasks(): Result<List<Task>> {
return Result.Success(tasksServiceData.values.toList())
}
override suspend fun saveTask(task: Task) {
tasksServiceData[task.id] = task
}
override suspend fun clearAllCompletedTasks() {
tasksServiceData = tasksServiceData.filterValues {
!it.completed
} as LinkedHashMap<String, Task>
}
override suspend fun deleteAllTasks() {
tasksServiceData.clear()
fetchAllToDoTasks()
}
override suspend fun deleteTask(taskId: String) {
tasksServiceData.remove(taskId)
fetchAllToDoTasks()
}
override suspend fun fetchAllToDoTasks(): Result<List<Task>> {
if(shouldReturnError) {
return Result.Error(Exception("Could not find task"))
}
val tasks = Result.Success(tasksServiceData.values.toList())
observableTasks.value = tasks
return tasks
}
override suspend fun updateLocalDataStore(list: List<Task>) {
TODO("Not yet implemented")
}
fun addTasks(vararg tasks: Task) {
tasks.forEach {
tasksServiceData[it.id] = it
}
runBlocking {
fetchAllToDoTasks()
}
}
}
Fragment class.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.loadTaskById(args.taskId)
setUpToast(this,viewModel.toastText)
viewModel.editTaskEvent.observe(viewLifecycleOwner, {
it?.let {
val action = TaskDetailFragmentDirections
.actionTaskDetailFragmentToAddEditFragment(
args.taskId,
resources.getString(R.string.edit_task)
)
findNavController().navigate(action)
}
})
binding.editTaskFab.setOnClickListener {
viewModel.editTask()
}
}
ViewModel class.
class TaskDetailViewModel(
private val tasksRepository: TasksRepository
) : ViewModel() {
private val TAG = "TaskDetailViewModel"
private val _taskId: MutableLiveData<String> = MutableLiveData()
private val _task = _taskId.switchMap {
tasksRepository.observeTask(it).map { res ->
Log.d("Test","res with value ${res.toString()}")
isolateTask(res)
}
}
val task: LiveData<Task?> = _task
private val _toastText = MutableLiveData<Int?>()
val toastText: LiveData<Int?> = _toastText
private val _dataLoading = MutableLiveData<Boolean>()
val dataLoading: LiveData<Boolean> = _dataLoading
private val _editTaskEvent = MutableLiveData<Unit?>(null)
val editTaskEvent: LiveData<Unit?> = _editTaskEvent
fun loadTaskById(taskId: String) {
if(dataLoading.value == true || _taskId.value == taskId) return
_taskId.value = taskId
Log.d("Test","loading task with id $taskId")
}
fun editTask(){
_editTaskEvent.value = Unit
}
fun setCompleted(completed: Boolean) = viewModelScope.launch {
val task = _task.value ?: return#launch
if(completed) {
tasksRepository.completeTask(task.id)
_toastText.value = R.string.task_marked_complete
}
else {
tasksRepository.activateTask(task.id)
_toastText.value = R.string.task_marked_active
}
}
private fun isolateTask(result: Result<Task?>): Task? {
return if(result is Result.Success) {
result.data
} else {
_toastText.value = R.string.loading_tasks_error
null
}
}
#Suppress("UNCHECKED_CAST")
class TasksDetailViewModelFactory(
private val tasksRepository: TasksRepository
): ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return (TaskDetailViewModel(
tasksRepository
) as T)
}
}
}
In this method in ViewModel when I comment out the below line of code all tests passed.
_toastText.value = R.string.loading_tasks_error
private fun isolateTask(result: Result<Task?>): Task? {
return if(result is Result.Success) {
result.data
} else {
_toastText.value = R.string.loading_tasks_error // Comment out this line then all test passed.
null
}
}
Problem
I got error message Cannot invoke setValue on a background thread when using MutableStateFlow on ActivityLifecycleCallback, i got this error after updating lifecycle jetpack to 2.4.0 and use repeatOnLifecycle. Calling network interfaces using viewmodel that injected by koin to activity responding in ActivityLifecycleCallback.
Implementation
Callback
class ActivityLifecycleCallback :
Utils.ActivityLifecycleCallbacks(),
KoinComponent {
private val mPref by inject<SecuredPrefUseCase>()
private var mViewModel: Lazy<AccountViewModel>? = null
override fun onActivityCreated(activity: Activity) {
super.onActivityCreated(activity)
if (activity is AppCompatActivity) {
mViewModel = activity.viewModel()
if (activity is AccountObserver.Interfaces) {
mViewModel?.let {
activity.lifecycle.addObserver(
AccountObserver(activity, it.value)
)
activity.supportFragmentManager.fragments.forEach { frg ->
if (frg is AccountObserver.Interfaces) {
frg.viewLifecycleOwner.lifecycle.addObserver(
AccountObserver(frg, it.value)
)
}
}
}
}
}
}
override fun onActivityStarted(activity: Activity) {
if (mPref.membership != MembershipType.GUEST) {
mViewModel?.value?.getMembership()
}
super.onActivityStarted(activity)
}
}
ViewModel
class AccountViewModel(
private val useCase: AccountUseCase,
) : BaseViewModel() {
private val _membership = MutableStateFlow<State.Single<Membership.Data?>>(State.Single.Idle())
val mMembership get() = _membership.asImmutable()
suspend fun resetMembership() {
_membership.emit(State.Single.Idle())
}
fun getMembership() = viewModelScope.launch(ioContext) {
requestSingle(_membership) {
useCase.getMembership()
}
}
suspend inline fun <R> BaseViewModel.requestSingle(
state: MutableStateFlow<State.Single<R>>,
delay: Long = Constant.MEDIUM_DELAY,
crossinline block: suspend () -> R,
) = mutex.withLock {
flow {
emit(State.Single.Loading())
val result = block.invoke()
delay(delay)
emit(State.Single.Success(result))
}.catch {
state.emit(State.Single.Failed(it))
}.collect {
state.emit(it)
}
}
}
Observer
class AccountObserver(
private val view: Interfaces,
private val viewModel: AccountViewModel,
) : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
owner.initObserver {
viewModel.mMembership.observeSingle(this) {
when (it) {
is State.Single.Idle -> view.onMembershipIdle()
is State.Single.Loading -> view.onMembershipLoading()
is State.Single.Success -> {
view.onMembershipSuccess(it.data)
viewModel.resetMembership()
}
is State.Single.Failed -> {
view.onMembershipFailed(it.throwable)
viewModel.resetMembership()
}
}
}
}
}
}
inline fun LifecycleOwner.initObserver(
crossinline block: suspend CoroutineScope.() -> Unit,
) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
block.invoke(this)
}
}
}
suspend inline fun <R> StateFlow<State.Single<R>>.observeSingle(
coroutineScope: CoroutineScope,
crossinline block: suspend (State.Single<R>) -> Unit,
) {
coroutineScope.launch {
collect {
block.invoke(it)
}
}
}
I've just finished my first Android App. It works as it should but, as you can imagine, there's a lot of spaghetti code and lack of performance. From what I've learned on Android and Kotlin language making this project (and a lot of articles/tutorials/SO answers) I'm trying to start it again from scratch to realize a better version. For now I'd like to keep it as simple as possible, just to better understand how to handle API calls with Retrofit and MVVM pattern, so no Volley/RXjava/Dagger etc.
I'm starting from the login obviously; I would like to make a post request to simply compare the credentials, wait for the response and, if positive, show a "loading screen" while fetching and processing data to show in the home page. I'm not storing any information so I have realized a singleton class that holds data as long as the app is running (btw, is there another way to do it?).
RetrofitService
private val retrofitService = Retrofit.Builder()
.addConverterFactory(
GsonConverterFactory
.create(
GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
.setLenient().setDateFormat("yyyy-MM-dd")
.create()
)
)
.addConverterFactory(RetrofitConverter.create())
.baseUrl(BASE_URL)
.build()
`object ApiObject {
val retrofitService: ApiInterface by lazy {
retrofitBuilder.create(ApiInterface::class.java) }
}
ApiInterface
interface ApiInterface {
#GET("workstation/{date}")
suspend fun getWorkstations(
#Path("date") date: Date
): List<Workstation>
#GET("reservation/{date}")
suspend fun getReservations(
#Path("date") date: Date
): List<Reservation>
#GET("user")
suspend fun getUsers(): List<User>
#GET("user/login")
suspend fun validateLoginCredentials(
#Query("username") username: String,
#Query("password") password: String
): Response<User>
ApiResponse
sealed class ApiResponse<T> {
companion object {
fun <T> create(response: Response<T>): ApiResponse<T> {
return if(response.isSuccessful) {
val body = response.body()
// Empty body
if (body == null || response.code() == 204) {
ApiSuccessEmptyResponse()
} else {
ApiSuccessResponse(body)
}
} else {
val msg = response.errorBody()?.string()
val errorMessage = if(msg.isNullOrEmpty()) {
response.message()
} else {
msg.let {
return#let JSONObject(it).getString("message")
}
}
ApiErrorResponse(errorMessage ?: "Unknown error")
}
}
}
}
class ApiSuccessResponse<T>(val data: T): ApiResponse<T>()
class ApiSuccessEmptyResponse<T>: ApiResponse<T>()
class ApiErrorResponse<T>(val errorMessage: String): ApiResponse<T>()
Repository
class Repository {
companion object {
private var instance: Repository? = null
fun getInstance(): Repository {
if (instance == null)
instance = Repository()
return instance!!
}
}
private var singletonClass = SingletonClass.getInstance()
suspend fun validateLoginCredentials(username: String, password: String) {
withContext(Dispatchers.IO) {
val result: Response<User>?
try {
result = ApiObject.retrofitService.validateLoginCredentials(username, password)
when (val response = ApiResponse.create(result)) {
is ApiSuccessResponse -> {
singletonClass.loggedUser = response.data
}
is ApiSuccessEmptyResponse -> throw Exception("Something went wrong")
is ApiErrorResponse -> throw Exception(response.errorMessage)
}
} catch (error: Exception) {
throw error
}
}
}
suspend fun getWorkstationsListFromService(date: Date) {
withContext(Dispatchers.IO) {
val workstationsListResult: List<Workstation>
try {
workstationsListResult = ApiObject.retrofitService.getWorkstations(date)
singletonClass.rWorkstationsList.postValue(workstationsListResult)
} catch (error: Exception) {
throw error
}
}
}
suspend fun getReservationsListFromService(date: Date) {
withContext(Dispatchers.IO) {
val reservationsListResult: List<Reservation>
try {
reservationsListResult = ApiObject.retrofitService.getReservations(date)
singletonClass.rReservationsList.postValue(reservationsListResult)
} catch (error: Exception) {
throw error
}
}
}
suspend fun getUsersListFromService() {
withContext(Dispatchers.IO) {
val usersListResult: List<User>
try {
usersListResult = ApiObject.retrofitService.getUsers()
singletonClass.rUsersList.postValue(usersListResult.let { usersList ->
usersList.filterNot { user -> user.username == "admin" }
.sortedWith(Comparator { x, y -> x.surname.compareTo(y.surname) })
})
} catch (error: Exception) {
throw error
}
}
}
SingletonClass
const val FAILED = 0
const val COMPLETED = 1
const val RUNNING = 2
class SingletonClass private constructor() {
companion object {
private var instance: SingletonClass? = null
fun getInstance(): SingletonClass {
if (instance == null)
instance = SingletonClass()
return instance!!
}
}
//User
var loggedUser: User? = null
//Workstations List
val rWorkstationsList = MutableLiveData<List<Workstation>>()
//Reservations List
val rReservationsList = MutableLiveData<List<Reservation>>()
//Users List
val rUsersList = MutableLiveData<List<User>>()
}
ViewModel
class ViewModel : ViewModel() {
private val singletonClass = SingletonClass.getInstance()
private val repository = Repository.getInstance()
//MutableLiveData
//Login
private val _loadingStatus = MutableLiveData<Boolean>()
val loadingStatus: LiveData<Boolean>
get() = _loadingStatus
private val _successfulAuthenticationStatus = MutableLiveData<Boolean>()
val successfulAuthenticationStatus: LiveData<Boolean>
get() = _successfulAuthenticationStatus
//Data fetch
private val _listsLoadingStatus = MutableLiveData<Int>()
val listsLoadingStatus: LiveData<Int>
get() = _listsLoadingStatus
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String>
get() = _errorMessage
fun onLoginClicked(username: String, password: String) {
launchLoginAuthentication {
repository.validateLoginCredentials(username, password)
}
}
private fun launchLoginAuthentication(block: suspend () -> Unit): Job {
return viewModelScope.launch {
try {
_loadingStatus.value = true
block()
} catch (error: Exception) {
_errorMessage.postValue(error.message)
} finally {
_loadingStatus.value = false
if (singletonClass.loggedUser != null)
_successfulAuthenticationStatus.value = true
}
}
}
fun onLoginPerformed() {
val date = Calendar.getInstance().time
launchListsFetch {
//how to start these all at the same time? Then wait until their competion
//and call the two methods below?
repository.getReservationsListFromService(date)
repository.getWorkstationsListFromService(date)
repository.getUsersListFromService()
}
}
private fun launchListsFetch(block: suspend () -> Unit): Job {
return viewModelScope.async {
try {
_listsLoadingStatus.value = RUNNING
block()
} catch (error: Exception) {
_listsLoadingStatus.value = FAILED
_errorMessage.postValue(error.message)
} finally {
//I'd like to perform these operations at the same time
prepareWorkstationsList()
prepareReservationsList()
//and, when both completed, set this value
_listsLoadingStatus.value = COMPLETED
}
}
}
fun onToastShown() {
_errorMessage.value = null
}
}
LoginActivity
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel
get() = ViewModelProviders.of(this).get(LoginViewModel::class.java)
private val loadingFragment = LoadingDialogFragment()
var username = ""
var password = ""
private lateinit var loginButton: Button
lateinit var context: Context
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
loginButton = findViewById(R.id.login_button)
loginButton.setOnClickListener {
username = login_username.text.toString().trim()
password = login_password.text.toString().trim()
viewModel.onLoginClicked(username, password.toMD5())
}
viewModel.loadingStatus.observe(this, Observer { value ->
value?.let { show ->
progress_bar_login.visibility = if (show) View.VISIBLE else View.GONE
}
})
viewModel.successfulAuthenticationStatus.observe(this, Observer { successfullyLogged ->
successfullyLogged?.let {
loadingFragment.setStyle(DialogFragment.STYLE_NORMAL, R.style.CustomLoadingDialogFragment)
if (successfullyLogged) {
loadingFragment.show(supportFragmentManager, "loadingFragment")
viewModel.onLoginPerformed()
} else {
login_password.text.clear()
login_password.isFocused
password = ""
}
}
})
viewModel.listsLoadingStatus.observe(this, Observer { loadingResult ->
loadingResult?.let {
when (loadingResult) {
COMPLETED -> {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
setResult(Activity.RESULT_OK)
finish()
}
FAILED -> {
loadingFragment.changeText("Error")
loadingFragment.showProgressBar(false)
loadingFragment.showRetryButton(true)
}
}
}
})
viewModel.errorMessage.observe(this, Observer { value ->
value?.let { message ->
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
viewModel.onToastShown()
}
})
}
Basically what I'm trying to do is to send username and password, show a progress bar while waiting for the result (if successful the logged user object is returned, otherwise a toast with the error message is shown), hide the progress bar and show the loading fragment. While showing the loading fragment start 3 async network calls and wait for their completion; when the third call is completed start the methods to elaborate the data and, when both done, start the next activity.
It seems to all works just fine, but debugging I've noticed the flow (basically network calls start/wait/onCompletion) is not at all like what I've described above. There's something to fix in the ViewModel, I guess, but I can't figure out what
How to use new Kotlin v1.3 coroutines in classes which do not have lifecycles, like repositories?
I have a class where I check if the cache is expired and then decide whether I fetch the data from the remote API or local database. I need to start the launch and async from there. But then how do I cancel the job?
Example code:
class NotesRepositoryImpl #Inject constructor(
private val cache: CacheDataSource,
private val remote: RemoteDataSource
) : NotesRepository, CoroutineScope {
private val expirationInterval = 60 * 10 * 1000L /* 10 mins */
private val job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
override fun getNotes(): LiveData<List<Note>> {
if (isOnline() && isCacheExpired()) {
remote.getNotes(object : GetNotesCallback {
override fun onGetNotes(data: List<Note>?) {
data?.let {
launch {
cache.saveAllNotes(it)
cache.setLastCacheTime(System.currentTimeMillis())
}
}
}
})
}
return cache.getNotes()
}
override fun addNote(note: Note) {
if (isOnline()) {
remote.createNote(note, object : CreateNoteCallback {
override fun onCreateNote(note: Note?) {
note?.let { launch { cache.addNote(it) } }
}
})
} else {
launch { cache.addNote(note) }
}
}
override fun getSingleNote(id: Int): LiveData<Note> {
if (isOnline()) {
val liveData: MutableLiveData<Note> = MutableLiveData()
remote.getNote(id, object : GetSingleNoteCallback {
override fun onGetSingleNote(note: Note?) {
note?.let {
liveData.value = it
}
}
})
return liveData
}
return cache.getSingleNote(id)
}
override fun editNote(note: Note) {
if (isOnline()) {
remote.updateNote(note, object : UpdateNoteCallback {
override fun onUpdateNote(note: Note?) {
note?.let { launch { cache.editNote(note) } }
}
})
} else {
cache.editNote(note)
}
}
override fun delete(note: Note) {
if (isOnline()) {
remote.deleteNote(note.id!!, object : DeleteNoteCallback {
override fun onDeleteNote(noteId: Int?) {
noteId?.let { launch { cache.delete(note) } }
}
})
} else {
cache.delete(note)
}
}
private fun isCacheExpired(): Boolean {
var delta = 0L
runBlocking(Dispatchers.IO) {
val currentTime = System.currentTimeMillis()
val lastCacheTime = async { cache.getLastCacheTime() }
delta = currentTime - lastCacheTime.await()
}
Timber.d("delta: $delta")
return delta > expirationInterval
}
private fun isOnline(): Boolean {
val runtime = Runtime.getRuntime()
try {
val ipProcess = runtime.exec("/system/bin/ping -c 1 8.8.8.8")
val exitValue = ipProcess.waitFor()
return exitValue == 0
} catch (e: IOException) {
e.printStackTrace()
} catch (e: InterruptedException) {
e.printStackTrace()
}
return false
}
}
You can create some cancelation method in the repository and call it from class which has lifecycle (Activity, Presenter or ViewModel), for example:
class NotesRepositoryImpl #Inject constructor(
private val cache: CacheDataSource,
private val remote: RemoteDataSource
) : NotesRepository, CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
fun cancel() {
job.cancel()
}
//...
}
class SomePresenter(val repo: NotesRepository) {
fun detachView() {
repo.cancel()
}
}
Or move launching coroutine to some class with lifecycle.