Delegate a job to a channelFLow - android

I'm trying to recreate this function using LiveData to a function doing the same but with channelFlow; Always set isLoading is boring to me. Rewrite a try finally everytime too.
LiveData way :
private val _isLoading: MutableLiveData<Boolean> = MutableLiveData(false)
val isLoading: LiveData<Boolean> = _isLoading
protected fun process(job: () -> Job) {
if (isLoading.value == true) return
_isLoading.value = true
viewModelScope.launch(Default) {
try {
job().join()
} finally {
withContext(Main) { _isLoading.value = false }
}
}
}
fun onSend() {
process {
viewModelScope.launch(Default) {
myRepository.insert("data the user had insert").let { isSuccessful ->
// do something
}
}
}
}
I tried this but cant make it work properly
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
private fun process(job: () -> Job): Flow<Any?> = channelFlow {
if (isLoading.value) return#channelFlow
_isLoading.value = true
try {
val data = job().join()
send(data)
} finally {
_isLoading.value = false
}
}
fun onSend(): Flow<Parcelable?> = process {
myRepository.insert("data the user had insert").let { result ->
// return the result
}
}
Any idea please ?

Related

How to until wait 2 parallel retrofit calls both finish?

I want to call 2 retrofit services in parallel and then do an action only when both of them finished, but I don't seem to figuer it out how.
I have a viewModel where I have defined my services:
var config= List<Configuration>
fun getClientProducts() {
getClientClientConfigUseCase
.build(this)
.executeWithError({ config ->
config = config
}, {
})
}
var therapies = List<DtoTherapy>
fun getTherapies() {
getTherapiesUseCase
.build(this)
.executeWithError({ config ->
therapies = it
}, {
})
}
And then I want to call both services in parallel in my fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupUi(view)
loadUserData()
viewModel.getClientProducts()
viewModel.getTherapies()
}
And when both variables config and therapies have the value do an action. But as I said maybe one service take 1 sec to respond and another 4 secs, and I want only to perfom an action when both have finished. Any help with be appreciated.
Here is the class I use to build the use case call:
abstract class SingleUseCase<T> : UseCase() {
private lateinit var single: Single<T>
private lateinit var useCaseInterface: UseCaseInterface
private var withLoader: Boolean = false
private var withErrorMessage: Boolean = false
internal abstract fun buildUseCaseSingle(): Single<T>
fun build(useCaseInterface: UseCaseInterface): SingleUseCase<T> {
this.withLoader = false
this.withErrorMessage = false
this.useCaseInterface = useCaseInterface
this.single = buildUseCaseSingle()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doAfterSuccess { useCaseInterface.onSuccess(it) }
return this
}
fun withLoader(): SingleUseCase<T> {
this.withLoader = true
return this
}
fun withErrorMessage(): SingleUseCase<T> {
this.withErrorMessage = true
return this
}
fun single(): Single<T> {
return this.single
}
fun execute(onSuccess: ((t: T) -> Unit)) {
useCaseInterface.onPrepareRequest(withLoader)
buildObservable(onSuccess)
}
private fun buildObservable(onSuccess: ((t: T) -> Unit)) {
disposeLast()
lastDisposable = single
.doFinally { useCaseInterface.onFinishRequest(this.withLoader) }
.subscribe(
{ onSuccess(it) },
{
useCaseInterface.onError(mapError(it), withErrorMessage)
})
lastDisposable?.let {
compositeDisposable.add(it)
}
}
fun executeWithError(onSuccess: ((success: T) -> Unit), onError: ((error: ApiError ) -> Unit)) {
useCaseInterface.onPrepareRequest(withLoader)
buildObservable(onSuccess, onError)
}
private fun buildObservable(onSuccess: ((success: T) -> Unit), onError: ((error: ApiError ) -> Unit)) {
disposeLast()
lastDisposable = single
.doFinally { useCaseInterface.onFinishRequest(this.withLoader) }
.subscribe(
{ onSuccess(it) },
{
onError(mapError(it))
useCaseInterface.onError(mapError(it), withErrorMessage)
})
lastDisposable?.let {
compositeDisposable.add(it)
}
}
private fun mapError(t: Throwable): ApiError {
return if(t is HttpException) {
val apiError = t.response()?.errorBody()?.string()
try {
ApiError (t.code(), t.response()?.errorBody()?.string(), Gson().fromJson(apiError, GenericError::class.java))
} catch(e: Exception) {
ApiError (-2, "Unkown error")
}
} else ApiError (-1, "Unkown error")
}
}
And this is a specific usecase class:
class GetClientConfigUseCase #Inject constructor(private val repository: UserRepository) :
SingleUseCase<ClientConfigResponse>() {
override fun buildUseCaseSingle(): Single<ClientConfigResponse> {
return repository.getUserConfig()
}
}
I guess you need zip operation. With zip operation you can have a result of two observable in one place when both of them received data.
Observable<List<ClientProducts>> observable1 = ...
Observable<List<DtoTherapy>> observable2 = ...
Observable.zip(observable1, observable2, new BiFunction<List<ClientProducts>, List<DtoTherapy>, Result>() {
#Override
public Result apply(List<ClientProducts> products, List<DtoTherapy> therapies) throws Exception
{
// here you have both of your data
// do operations on products and therapies
// then return the result
return result;
}
});

Unable to update content of recycleview based on user's location

I've been experiencing a strange bug when retrieving a user location and displaying data based on the location retrieved in a recycler view. For context, whenever the application is started from fresh (no permissions granted) can retrieve the location. However, it doesn't display the expected content in the recycler view unless I close the application or press the bottom navigation bar.
Demonstration:
https://i.imgur.com/9kc1Zxc.gif
Fragment
const val TAG = "ForecastFragment"
#AndroidEntryPoint
class ForecastFragment : Fragment(), SearchView.OnQueryTextListener,
SwipeRefreshLayout.OnRefreshListener{
private val viewModel: WeatherForecastViewModel by viewModels()
private var _binding: FragmentForecastBinding? = null
private val binding get() = _binding!!
private lateinit var forecastAdapter: ForecastAdapter
private lateinit var searchMenuItem: MenuItem
private lateinit var searchView: SearchView
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
private var currentQuery: String? = null
private lateinit var client: FusedLocationProviderClient
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.forecast_list_menu, menu)
searchMenuItem = menu.findItem(R.id.menu_search)
searchView = searchMenuItem.actionView as SearchView
searchView.isSubmitButtonEnabled = true
searchView.setOnQueryTextListener(this)
return super.onCreateOptionsMenu(menu, inflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentForecastBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
client = LocationServices.getFusedLocationProviderClient(requireActivity())
swipeRefreshLayout = binding.refreshLayoutContainer
setupRecyclerView()
getLastLocation()
updateSupportActionbarTitle()
swipeRefreshLayout.setOnRefreshListener(this)
return binding.root
}
private fun updateSupportActionbarTitle() {
viewModel.queryMutable.observe(viewLifecycleOwner, Observer { query ->
(activity as AppCompatActivity).supportActionBar?.title = query
})
}
private fun setupRecyclerView() {
forecastAdapter = ForecastAdapter()
binding.forecastsRecyclerView.apply {
adapter = forecastAdapter
layoutManager = LinearLayoutManager(activity)
}
}
private fun requestWeatherApiData() {
lifecycleScope.launch {
viewModel.weatherForecast.observe(viewLifecycleOwner, Observer { response ->
when (response) {
is Resource.Success -> {
response.data?.let { forecastResponse ->
forecastAdapter.diff.submitList(forecastResponse.list)
swipeRefreshLayout.isRefreshing = false
}
}
is Resource.Error -> {
response.msg?.let { msg ->
Log.e(TAG, "An error occurred : $msg")
swipeRefreshLayout.isRefreshing = false
}
}
is Resource.Loading -> {
// nothing for now
swipeRefreshLayout.isRefreshing = false
}
}
})
}
}
private fun searchWeatherApiData(searchQuery: String) {
viewModel.searchWeatherForecast(viewModel.applySearchQuery(searchQuery))
viewModel.searchWeatherForecastResponse.observe(viewLifecycleOwner, Observer { response ->
when (response) {
is Resource.Success -> {
response.data?.let { forecastResponse ->
forecastAdapter.diff.submitList(forecastResponse.list)
swipeRefreshLayout.isRefreshing = false
}
}
is Resource.Error -> {
response.msg?.let { msg ->
Log.e(TAG, "An error occurred : $msg")
swipeRefreshLayout.isRefreshing = false
}
}
is Resource.Loading -> {
swipeRefreshLayout.isRefreshing = false
}
}
})
}
private fun getWeatherApiDataLocation(city: String) {
viewModel.weatherForecastLocation(viewModel.applyLocationQuery(city))
viewModel.weatherForecastLocation.observe(viewLifecycleOwner, Observer { response ->
when (response) {
is Resource.Success -> {
response.data?.let { forecastResponse ->
forecastAdapter.diff.submitList(forecastResponse.list)
forecastAdapter.notifyDataSetChanged()
swipeRefreshLayout.isRefreshing = false
}
}
is Resource.Error -> {
response.msg?.let { msg ->
Log.e(TAG, "An error occurred : $msg")
swipeRefreshLayout.isRefreshing = false
}
}
is Resource.Loading -> {
swipeRefreshLayout.isRefreshing = false
}
}
})
}
private fun fetchForecastAsync(query: String?) {
if (query == null)
requestWeatherApiData()
else
searchWeatherApiData(query)
}
override fun onQueryTextSubmit(query: String?): Boolean {
if (query != null) {
currentQuery = query
viewModel.queryMutable.value = query
searchWeatherApiData(query)
}
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
return true
}
override fun onRefresh() {
currentQuery = viewModel.queryMutable.value
fetchForecastAsync(currentQuery)
}
private fun isLocationEnabled(): Boolean {
val locationManager: LocationManager =
requireActivity().getSystemService(Context.LOCATION_SERVICE) as LocationManager
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
|| locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
}
private fun checkPermissions(): Boolean {
if (ActivityCompat.checkSelfPermission(requireContext(),
Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(requireContext(),
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED){
return true
}
return false
}
private fun requestPermissions() {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION), PERMISSION_ID)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
if (requestCode == PERMISSION_ID) {
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
getLastLocation()
}
}
}
#SuppressLint("MissingPermission")
private fun getLastLocation() {
if (checkPermissions()) {
if (isLocationEnabled()) {
client.lastLocation.addOnCompleteListener(requireActivity()) { task ->
val location: Location? = task.result
if (location == null) {
requestNewLocationData()
} else {
val geoCoder = Geocoder(requireContext(), Locale.getDefault())
val addresses = geoCoder.getFromLocation(
location.latitude, location.longitude, 1)
val city = addresses[0].locality
viewModel.queryMutable.value = city
getWeatherApiDataLocation(city)
}
}
} else {
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
startActivity(intent)
}
} else {
requestPermissions()
}
}
#SuppressLint("MissingPermission")
private fun requestNewLocationData() {
val mLocationRequest = LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
interval = 0
fastestInterval = 0
numUpdates = 1
}
client = LocationServices.getFusedLocationProviderClient(requireActivity())
client.requestLocationUpdates(
mLocationRequest, mLocationCallback,
Looper.myLooper()
)
}
private val mLocationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
val mLastLocation: Location = locationResult.lastLocation
mLastLocation.latitude.toString()
mLastLocation.longitude.toString()
}
}
}
View Model
#HiltViewModel
class WeatherForecastViewModel
#Inject constructor(
private val repository: WeatherForecastRepository,
application: Application) :
AndroidViewModel(application) {
private val unit = "imperial"
private var query = "Paris"
val weatherForecast: MutableLiveData<Resource<WeatherForecastResponse>> = MutableLiveData()
var searchWeatherForecastResponse: MutableLiveData<Resource<WeatherForecastResponse>> = MutableLiveData()
val weatherForecastLocation: MutableLiveData<Resource<WeatherForecastResponse>> = MutableLiveData()
var queryMutable: MutableLiveData<String> = MutableLiveData()
init {
//queryMutable.value = query
//getWeatherForecast(queryMutable.value.toString(), unit)
}
private fun getWeatherForecast(query: String, units: String) =
viewModelScope.launch {
weatherForecast.postValue(Resource.Loading())
val response = repository.getWeatherForecast(query, units)
weatherForecast.postValue(weatherForecastResponseHandler(response))
}
private suspend fun searchWeatherForecastSafeCall(searchQuery: Map<String, String>) {
searchWeatherForecastResponse.postValue(Resource.Loading())
val response = repository.searchWeatherForecast(searchQuery)
searchWeatherForecastResponse.postValue(weatherForecastResponseHandler(response))
}
private suspend fun getWeatherForecastLocationSafeCall(query: Map<String, String>) {
weatherForecastLocation.postValue(Resource.Loading())
val response = repository.getWeatherForecastLocation(query)
weatherForecastLocation.postValue(weatherForecastResponseHandler(response))
}
fun searchWeatherForecast(searchQuery: Map<String, String>) =
viewModelScope.launch {
searchWeatherForecastSafeCall(searchQuery)
}
fun weatherForecastLocation(query: Map<String, String>) =
viewModelScope.launch {
getWeatherForecastLocationSafeCall(query)
}
fun applySearchQuery(searchQuery: String): HashMap<String, String> {
val queries: HashMap<String, String> = HashMap()
queries[QUERY_CITY] = searchQuery
queries[QUERY_UNITS] = unit
queries[QUERY_COUNT] = API_COUNT
queries[QUERY_API] = API_KEY
return queries
}
fun applyLocationQuery(query: String): HashMap<String, String> {
val queries: HashMap<String, String> = HashMap()
queries[QUERY_CITY] = query
queries[QUERY_UNITS] = unit
queries[QUERY_COUNT] = API_COUNT
queries[QUERY_API] = API_KEY
return queries
}
private fun weatherForecastResponseHandler(
response: Response<WeatherForecastResponse>): Resource<WeatherForecastResponse> {
if (response.isSuccessful) {
response.body()?.let { result ->
return Resource.Success(result)
}
}
return Resource.Error(response.message())
}
}
I feel the problem lies in the way, you are setting up your observers, you are setting them up in a function call, I believe that is wrong and that is setting up multiple observers for the same thing. It should just be setup once.
So I suggest you take out your observers to a separate function and call it once like observeProperties()
So your code will be like this
private fun observeProperties() {
viewModel.searchWeatherForecastResponse.observe(viewLifecycleOwner, Observer { response ->
when (response) {
is Resource.Success -> {
response.data?.let { forecastResponse ->
forecastAdapter.diff.submitList(forecastResponse.list)
swipeRefreshLayout.isRefreshing = false
}
}
is Resource.Error -> {
response.msg?.let { msg ->
Log.e(TAG, "An error occurred : $msg")
swipeRefreshLayout.isRefreshing = false
}
}
is Resource.Loading -> {
swipeRefreshLayout.isRefreshing = false
}
}
})
viewModel.weatherForecastLocation.observe(viewLifecycleOwner, Observer { response ->
when (response) {
is Resource.Success -> {
response.data?.let { forecastResponse ->
forecastAdapter.diff.submitList(forecastResponse.list)
forecastAdapter.notifyDataSetChanged()
swipeRefreshLayout.isRefreshing = false
}
}
is Resource.Error -> {
response.msg?.let { msg ->
Log.e(TAG, "An error occurred : $msg")
swipeRefreshLayout.isRefreshing = false
}
}
is Resource.Loading -> {
swipeRefreshLayout.isRefreshing = false
}
}
})
viewModel.searchWeatherForecastResponse.observe(viewLifecycleOwner, Observer { response ->
when (response) {
is Resource.Success -> {
response.data?.let { forecastResponse ->
forecastAdapter.diff.submitList(forecastResponse.list)
swipeRefreshLayout.isRefreshing = false
}
}
is Resource.Error -> {
response.msg?.let { msg ->
Log.e(TAG, "An error occurred : $msg")
swipeRefreshLayout.isRefreshing = false
}
}
is Resource.Loading -> {
swipeRefreshLayout.isRefreshing = false
}
}
})
}
Now you can call this method at the top as such
setupRecyclerView()
getLastLocation()
updateSupportActionbarTitle()
swipeRefreshLayout.setOnRefreshListener(this)
observeProperties()
Your other methods simply will be now the queries like this
private fun getWeatherApiDataLocation(city: String) {
viewModel.weatherForecastLocation(viewModel.applyLocationQuery(city))
}
private fun searchWeatherApiData(searchQuery: String) {
viewModel.searchWeatherForecast(viewModel.applySearchQuery(searchQuery))
}

Mockito never() get called even when the method is not called?

Here's my Repo
interface TrendingRepository{
fun getTrendingRepos()
Test Class
#RunWith(JUnit4::class)
class TrendingViewModelTest {
private val trendingRepository = mock(TrendingRepository::class.java)
private var trendingViewModel = TrendingViewModel(trendingRepository)
#Test
fun testWithNoNetwork() {
trendingViewModel.isConnected = false
verify(trendingRepository, never()).getTrendingRepos()
}
#Test
fun testWithNetwork() {
trendingViewModel.isConnected = true
verify(trendingRepository, never()).getTrendingRepos()
}
}
TrendingViewModel
fun fetchTrendingRepos() {
if (isConnected) {
loadingProgress.value = true
compositeDisposable.add(
trendingRepository.getTrendingRepos().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ response ->
run {
loadingProgress.value = false
},
{ error ->
loadingProgress.value = false
}
)
)
}
Both these test are passing, however TrendingViewModel is only calling getTrendingRepos() only when there is active network, isConnected = true
You should call function that you're testing before verification:
#RunWith(JUnit4::class)
class TrendingViewModelTest {
private val trendingRepository = mock(TrendingRepository::class.java)
private var trendingViewModel = TrendingViewModel(trendingRepository)
#Test
fun testWithNoNetwork() {
trendingViewModel.isConnected = false
trendingViewModel.fetchTrendingRepos()
verify(trendingRepository, never()).getTrendingRepos()
}
#Test
fun testWithNetwork() {
trendingViewModel.isConnected = true
trendingViewModel.fetchTrendingRepos()
verify(trendingRepository, times(1)).getTrendingRepos()
}
}

Make android MVVM, Kotlin Coroutines and Retrofit 2.6 work asynchronously

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

Android : Testing viewmodel with coroutines

I have this testing method
private var testDispatcher = TestCoroutineDispatcher()
#Test
fun test__success() {
viewModel = MyActivateViewModel(
"LG_NAME",
"EXH_NAME",
"_CODE",
"C_CODE", controller)
every {
controller.activate(any(), any())
} returns true
testDispatcher
.runBlockingTest {
viewModel.onOkButtonClicked()
Truth.assertThat(viewModel.activated.value).isEqualTo(true)
}
}
fun onOkButtonClicked() {
viewModelScope.launch {
val status = activateTask()
if (status == 0) {
activated.value = true
} else {
activationFailed.value = status
}
}
}
private suspend fun activateTask(): Int {
return withContext(Dispatchers.IO) {
var status = 0
try {
controller.activate(code, code)
} catch (e: LoginException) {
status = e.reason
}
status
}
}
Since I am calling runBlockingTest {
viewModel.onOkButtonClicked()
I expect this to be completed. But the assert below these lines are failing. What i see is the coroutine is not finished before the assert line is executed. If i add a delay before assert, it works as expected.
What is wrong in my implemetation?
what is the right way

Categories

Resources