I am learning clean architecture and Kotlin Flow. I want to check is user mail exists in the Firebase Auth base. However, when I threw an error to the flow function, app is crash.
CheckUserUseCase.kt
class CheckUserUseCase #Inject constructor(private val repository: SignInRepository) {
operator fun invoke(mail: String): Flow<Status<Boolean, String>> = flow {
emit(Status.Loading(data = null))
try {
repository.isUserExists(mail = mail)
emit(Status.Success(data = true))
} catch (e: Exception) {
emit(Status.Error(message = e.message, data = false))
}
}
}
SignInRepository.kt
interface SignInRepository {
suspend fun isUserExists(mail: String)
}
SignInRepositoryImpl.kt
class SignInRepositoryImpl #Inject constructor(private val firebaseUserActions: FirebaseUserActions) : SignInRepository {
override suspend fun isUserExists(mail: String) {
firebaseUserActions.isUserExists(mail = mail)
}
}
FirebaseAuthentication.kt
class FirebaseAuthentication #Inject constructor(private val auth: FirebaseAuth) : FirebaseUserActions {
override suspend fun isUserExists(mail: String){
auth.fetchSignInMethodsForEmail(mail).addOnCompleteListener { task ->
task.result.signInMethods?.let {
if (it.size != 0) Log.i("App.tag", "True.")
else throw IOException() <-- Crash point.
}
}.addOnFailureListener { e -> e.printStackTrace() }
.await()
}
}
How can I return a state to Kotlin Flow method? Thank you!
Please try the following approach:
override suspend fun isUserExists(mail: String): Status {
return try {
val result = auth.fetchSignInMethodsForEmail(mail).await()
result.signInMethods?.let {
if (it.isNotEmpty()) {
Status.Success(data = true)
} else {
Status.Error(message = "No data", data = false)
}
} ?: Status.Error(message = "No Data", data = false)
} catch (e: Exception) {
Status.Error(message = e.message, data = false)
}
}
In CheckUserUseCase class just emit the result of calling isUserExists():
emit(Status.Loading(data = null))
emit(repository.isUserExists(mail = mail))
Try
it.size != 0 && it.size != null
and
if (task.isSuccessful()) {
[...]
task.result.signInMethods?.let {
[...]
}
Related
I'm trying to get access token and refresh token from my app. I'm using one tap sign in but I cant seem to get the desired access and refresh tokens (for Oauth2) .One tap works just fine in both signIn/signUp. I tried using the following:
Module:
#Provides
fun provideOneTapClient(
context: Context
) = Identity.getSignInClient(context)
#Provides
#Named("signInRequest")
fun provideSignInRequest(
app: Application
) = BeginSignInRequest.builder()
.setGoogleIdTokenRequestOptions(
BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
.setSupported(true)
.setServerClientId(app.getString(R.string.web_client_id))
.setFilterByAuthorizedAccounts(true)
.build()
)
.setAutoSelectEnabled(true)
.build()
#Provides
#Named("signUpRequest")
fun provideSignUpRequest(
app: Application
) = BeginSignInRequest.builder()
.setGoogleIdTokenRequestOptions(
BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
.setSupported(true)
.setServerClientId(app.getString(R.string.web_client_id))
.setFilterByAuthorizedAccounts(false)
.build()
)
.build()
#Provides
fun provideGoogleSignInOptions(
app: Application
) = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(app.getString(R.string.web_client_id))
.requestScopes(Scope(CalendarScopes.CALENDAR))
.requestServerAuthCode(app.getString(R.string.web_client_id), true)
.requestEmail()
.build()
#Provides
fun provideGoogleSignInClient(
app: Application,
options: GoogleSignInOptions
) = GoogleSignIn.getClient(app, options)
#Provides
fun provideAuthRepository(
auth: FirebaseAuth,
oneTapClient: SignInClient,
#Named("signInRequest")
signInRequest: BeginSignInRequest,
#Named("signUpRequest")
signUpRequest: BeginSignInRequest,
signInClient: GoogleSignInClient,
usersRef: CollectionReference
): AuthRepository = AuthRepositoryImpl(
auth = auth,
oneTapClient = oneTapClient,
signInRequest = signInRequest,
signUpRequest = signUpRequest,
signInClient = signInClient,
usersRef = usersRef
)
AuthImpl:
override suspend fun oneTapSignInWithGoogle() = flow {
try {
emit(Loading)
val result = oneTapClient.beginSignIn(signInRequest).await()
emit(Success(result))
} catch (e: Exception) {
emit(Failure(e))
}
}
override suspend fun oneTapSignUpWithGoogle() = flow {
try {
emit(Loading)
val result = oneTapClient.beginSignIn(signUpRequest).await()
emit(Success(result))
} catch (e: Exception) {
emit(Failure(e))
}
}
override suspend fun firebaseSignInWithGoogle(googleCredential: AuthCredential) = flow {
try {
emit(Loading)
val authResult = auth.signInWithCredential(googleCredential).await()
val isNewUser = authResult.additionalUserInfo?.isNewUser
emit(Success(isNewUser))
} catch (e: Exception) {
emit(Failure(e))
}
}
viewmodel:
override suspend fun oneTapSignInWithGoogle() = flow {
try {
emit(Loading)
val result = oneTapClient.beginSignIn(signInRequest).await()
emit(Success(result))
} catch (e: Exception) {
emit(Failure(e))
}
}
override suspend fun oneTapSignUpWithGoogle() = flow {
try {
emit(Loading)
val result = oneTapClient.beginSignIn(signUpRequest).await()
emit(Success(result))
} catch (e: Exception) {
emit(Failure(e))
}
}
override suspend fun firebaseSignInWithGoogle(googleCredential: AuthCredential) = flow {
try {
emit(Loading)
val authResult = auth.signInWithCredential(googleCredential).await()
val isNewUser = authResult.additionalUserInfo?.isNewUser
emit(Success(isNewUser))
} catch (e: Exception) {
emit(Failure(e))
}
}
SignIn Screen:
val launcher = rememberLauncherForActivityResult(StartIntentSenderForResult()) { result ->
if (result.resultCode == RESULT_OK) {
try {
val credentials = viewModel.oneTapClient.getSignInCredentialFromIntent(result.data)
val googleIdToken = credentials.googleIdToken
val googleCredentials = getCredential(googleIdToken, null)
viewModel.signInWithGoogle(googleCredentials)
} catch (it: ApiException) {
print(it)
}
}
}
fun launch(signInResult: BeginSignInResult) {
val intent = IntentSenderRequest.Builder(signInResult.pendingIntent.intentSender).build()
launcher.launch(intent)
}
when (val oneTapSignInResponse = viewModel.oneTapSignInState.value) {
is Loading -> ProgressBar()
is Success -> oneTapSignInResponse.data?.let {
LaunchedEffect(it) {
launch(it)
}
}
is Failure -> oneTapSignInResponse.e?.let {
LaunchedEffect(Unit) {
print(it)
if (it.message == "16: Cannot find a matching credential.") {
viewModel.oneTapSignUp()
}
}
}
}
when (val oneTapSignUpResponse = viewModel.oneTapSignUpState.value) {
is Loading -> ProgressBar()
is Success -> oneTapSignUpResponse.data?.let {
LaunchedEffect(it) {
launch(it)
}
}
is Failure -> oneTapSignUpResponse.e?.let {
LaunchedEffect(Unit) {
print(it)
}
}
}
when (val signInResponse = viewModel.signInState.value) {
is Loading -> ProgressBar()
is Success -> signInResponse.data?.let { isNewUser ->
if (isNewUser) {
LaunchedEffect(isNewUser) {
viewModel.createUser()
}
} else {
LaunchedEffect(Unit) {
navController.navigate(EisenhowerScreens.EisenHomeScreen.name) {
popUpTo(EisenhowerScreens.LoginScreen.name) {
inclusive = true
}
}
}
}
}
is Failure -> signInResponse.e?.let {
LaunchedEffect(Unit) {
print(it)
}
}
}
I'm using jetpack compose for the app. and I'm not sure what I need to have Access and Refresh tokens.
I have multiple API requests that need to be called in parallel, the sequence doesn't matter.
What really matters is that all the calls should be requested to implement the UI.
The problem is that sometimes some requests don't get called which returns null values, in other words, NO GUARANTEE THAT ALL THE REQUESTS WILL BE CALLED.
I have read a lot about parallel API requests and launchers but still don't know what I have missed or what I did wrong.
here is my view model class that has all functions
#HiltViewModel
class MatchDetailsViewModel #Inject constructor(
val api:FootballApi,
val app: Application,
): AndroidViewModel(app) {
private val _matchDetailsMutableLiveData = MutableLiveData<ResponseState<FixtureById>>()
private val _matchStatsMutableLiveData = MutableLiveData<ResponseState<Stats>>()
private val _matchLineupsMutableLiveData = MutableLiveData<ResponseState<Lineups>>()
private val _matchBenchMutableLiveData = MutableLiveData<ResponseState<Bench>>()
private val _matchSideLinesMutableLiveData = MutableLiveData<ResponseState<SideLine>>()
private val _matchStandingsMutableLiveData = MutableLiveData<ResponseState<Standings>>()
val matchStandingsLiveData: LiveData<ResponseState<Standings>> = _matchStandingsMutableLiveData
val matchSideLinesLiveData: LiveData<ResponseState<SideLine>> = _matchSideLinesMutableLiveData
val matchStatsLiveData: LiveData<ResponseState<Stats>> = _matchStatsMutableLiveData
val matchDetailsLiveData: LiveData<ResponseState<FixtureById>> = _matchDetailsMutableLiveData
val matchLineupsLiveData: LiveData<ResponseState<Lineups>> = _matchLineupsMutableLiveData
val matchBenchLiveData: LiveData<ResponseState<Bench>> = _matchBenchMutableLiveData
fun callAll() {
viewModelScope.launch {
val getMatchDetailsCall = async { getMatchDetails(1582601) }
val getMatchStatsCall = async { getMatchStats(1582601) }
val getMatchLineupsCall = async { getMatchLineups(1582601) }
val getMatchBenchCall = async { getMatchBench(1582601) }
val getMatchSideLineCall = async { getMatchSideLine(15006543) }
val getMatchStandingsCall = async { getMatchStandings(12880) }
try {
getMatchDetailsCall.await()
getMatchStatsCall.await()
getMatchLineupsCall.await()
getMatchBenchCall.await()
getMatchSideLineCall.await()
getMatchStandingsCall.await()
}
catch (_: Exception){}
}
}
suspend fun getMatchStandings(seasonId: Int) = viewModelScope.launch(Dispatchers.IO) {
_matchStandingsMutableLiveData.postValue(ResponseState.Loading())
try {
val response = api.getMatchStandings(seasonId = seasonId)
Log.i("getMatchStanding()", response.body().toString())
_matchStandingsMutableLiveData.postValue(ResponseState.Success(response.body()!!))
}
catch (exception: Exception) {
Log.e("getMatchStanding()", exception.toString())
}
}
suspend fun getMatchSideLine(id: Int) = viewModelScope.launch(Dispatchers.IO) {
_matchSideLinesMutableLiveData.postValue(ResponseState.Loading())
try {
val response = api.getMatchSideLines(id = id)
Log.i("getMatchSideline()", response.body().toString())
_matchSideLinesMutableLiveData.postValue(ResponseState.Success(response.body()!!))
} catch (exception: Exception) {
Log.e("getMatchSideline()", exception.toString())
}
}
fun getMatchDetails(id: Int) = viewModelScope.launch(Dispatchers.IO) {
_matchDetailsMutableLiveData.postValue(ResponseState.Loading())
try {
val response = api.getMatchDetails(id = id)
Log.i("getMatchDetails()", response.body().toString())
_matchDetailsMutableLiveData.postValue(ResponseState.Success(response.body()!!))
} catch (exception: Exception) {
Log.e("getMatchDetails()", exception.toString())
}
}
suspend fun getMatchStats(id: Int) = viewModelScope.launch(Dispatchers.IO) {
_matchStatsMutableLiveData.postValue(ResponseState.Loading())
try {
val response = api.getMatchStats(id = id)
Log.i("getMatchStats()", response.body().toString())
_matchStatsMutableLiveData.postValue(ResponseState.Success(response.body()!!))
} catch (exception: Exception) {
Log.e("getMatchStats()", exception.toString())
}
}
suspend fun getMatchLineups(id: Int = 6) = viewModelScope.launch(Dispatchers.IO) {
_matchLineupsMutableLiveData.postValue(ResponseState.Loading())
try {
val response = api.getMatchLineups(id = id)
Log.i("getMatchLineups()", response.body().toString())
_matchLineupsMutableLiveData.postValue(ResponseState.Success(response.body()!!))
} catch (exception: Exception) {
Log.e("getMatchLineups()", exception.toString())
}
}
suspend fun getMatchBench(id: Int) = viewModelScope.launch(Dispatchers.IO) {
_matchBenchMutableLiveData.postValue(ResponseState.Loading())
try {
val response = api.getMatchBench(id = id)
Log.i("getMatchBench()", response.body().toString())
_matchBenchMutableLiveData.postValue(ResponseState.Success(response.body()!!))
} catch (exception: Exception) {
Log.e("getMatchBench()", exception.toString())
}
}
}
And here's my call in activity class
lifecycleScope.launch(Dispatchers.IO) {
matchDetailsViewModel.callAll()
}
I have implemented MVVM architecture in one of my projects. In which I have created a ViewModel, Repository, and ViewModelFactory to make API call. I'm showing a list of products on the wish list. There is a facility where you can remove a particular product from the wish list. When my activity opens its API calls and renders the data but when I remove the product from the recycler view list need to make an API call which gets the remaining list of products available in the wishlist. When I change the configuration of the device it also makes API calls. How can I survive the configuration changes?
Below is the code from Activity
val repository = WishlistRepository(activity)
wishlistViewModel = ViewModelProvider(
this,
WishlistViewModelFactory(repository)
)[WishlistViewModel::class.java]
wishlistObservables()
if (preferenceUtils.getPrefBoolean(Constants.IS_LOGGED_IN)) {
if (wishlistViewModel.wishListLiveData.value == null) {
prepareParams()
}
} else {
val intent = Intent(activity, LoginActivity::class.java)
intent.putExtra("fromScreen", 2)
startActivity(intent)
}
prepareParams method
private fun prepareParams() {
if (Utils.isNetworkAvailable(activity)) {
val params = mutableMapOf<String, String>()
params["customer_id"] = preferenceUtils.getPreString(Constants.USER_ID)
params["store"] = preferenceUtils.getPreInt(Constants.STORE).toString()
wishlistViewModel.getWishList(params)
} else {
Utils.snackBar(binding.coordinatorWishlist, getString(R.string.network))
}
}
ViewModel Code
val wishListLiveData: LiveData<Resource<WishlistModel>> get() = repository.wishLiveData
fun getWishList(params: Map<String, String>) {
repository.getWishlistApiCall(params)
}
Repository Code
private val wishListLiveData = MutableLiveData<Resource<WishlistModel>>()
val wishLiveData:LiveData<Resource<WishlistModel>> get() = wishListLiveData
fun getWishlistApiCall(params:Map<String,String>){
if (Utils.isNetworkAvailable(activity)) {
wishListLiveData.postValue(Resource.Loading())
retroService.getWishListApiCall(params).enqueue(object : Callback<WishlistModel> {
override fun onResponse(call: Call<WishlistModel>, response: Response<WishlistModel>) {
try {
if (response.body() != null) {
if (response.isSuccessful) {
response.body()?.let {
if (it.code == 200) {
if (it.status == "1") {
wishListLiveData.postValue(Resource.Success(it))
} else {
wishListLiveData.postValue(Resource.Error(it.message))
}
} else if (it.code == 404) {
wishListLiveData.postValue(
Resource.Error(
activity.getString(
R.string.four_hundred_four_not_found
)
)
)
} else if (it.code == 500) {
wishListLiveData.postValue(
Resource.Error(
activity.getString(
R.string.five_hundred_server_error
)
)
)
} else {
wishListLiveData.postValue(Resource.Error(it.message))
}
}
} else {
wishListLiveData.postValue(Resource.Error(response.message()))
}
} else {
wishListLiveData.postValue(Resource.Error(response.message()))
}
} catch (e: Exception) {
wishListLiveData.postValue(Resource.Error(e.message.toString()))
}
}
override fun onFailure(call: Call<WishlistModel>, t: Throwable) {
try {
wishListLiveData.postValue(Resource.Error(t.message.toString()))
} catch (e: Exception) {
wishListLiveData.postValue(Resource.Error(e.message.toString()))
}
}
})
} else {
wishListLiveData.postValue(Resource.Error(activity.getString(R.string.network)))
}
}
ViewModelFactory Code
class WishlistViewModelFactory(private val repository: WishlistRepository) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return WishlistViewModel(repository) as T
}
}
Remove wishlist observables
private fun removeObservables() {
removeWishlistViewModel.removeWishListLiveData.observe(this) {
when (it.status) {
Status.LOADING -> {
Utils.showDialog(activity)
}
Status.SUCCESS -> {
Utils.hideDialog()
if (it.data != null) {
it.data.let { removeWishModel ->
Utils.snackBar(
binding.coordinatorWishlist,
removeWishModel.message, Constants.SNACK_SUCCESS_COLOR
)
val activityFragmentMessageEvent =
Events.ActivityFragmentMessage("Wishlist")
EventBus.getDefault().post(activityFragmentMessageEvent)
prepareParams()
}
}
}
Status.ERROR -> {
Utils.hideDialog()
Utils.snackBar(binding.coordinatorWishlist, it.errorMessage.toString())
}
}
}
}
When I call prepareParams method again it's will make an API call of getting the wish-listed products. After that when I change the configuration from portrait to landscape it's starting again to make API calls again and again.
How can I resolve this issue? Please help me to resolve this issue with MVVM. I'm begginner in MVVM architecture.
Thank you
I am debugging an application that communicates with an IoT device via http.
In response to commands, the device sends information in xml format.
An application can also receive binary data on a GET request.
In the functionality of the application, filling the RecyclerView from the list and loading images to fill the RecyclerView and executing individual commands to change modes.
The problem is that the device does not have the most powerful processor, and when a large number of http commands are received, the service cannot cope and hangs for a long time until the WiFi channel fails.
I can’t figure out how to organize interaction so that each next command waits for the previous one to complete. The solution is complicated by the fact that populating the RecyclerView, loading images, and executing commands are in different parts of the code, and each is executed asynchronously.
Populating RecyclerView:
private fun initViewModel(filter: String) {
val st = Storage(requireContext())
val cache = "${st.externalCacheDir}/$filter/"
val viewModel = ViewModelProvider(this).get(DeviceListViewModel::class.java)
viewModel.getRecycerListObserver().observe(requireActivity(), Observer<ResponseData> {
if (it != null) {
val media = it.mediaData?.filter { it.mediaData?.fPath!!.contains(filter, false) }
mediaList = arrayListOf()
if (media != null) {
for (i in media.sortedByDescending { it.mediaData?.fTimeCode }) {
i.mediaData?.let { it1 -> mediaList.add(it1) }
}
}
viewModel.recyclerListLiveData = MutableLiveData()
ThumbDownloader(dataAdapter, mediaList, cache, swipeLayout).execute()
} else {
Toast.makeText(activity, "Error in getting data", Toast.LENGTH_SHORT).show()
}
})
viewLifecycleOwner.lifecycleScope.launch {
viewModel.makeApiCall()
}
}
ViewModel:
class DeviceListViewModel : ViewModel() {
var recyclerListLiveData: MutableLiveData<ResponseData>
init {
recyclerListLiveData = MutableLiveData()
}
fun getRecycerListObserver(): MutableLiveData<ResponseData> {
return recyclerListLiveData
}
fun makeApiCall() {
viewModelScope.launch(Dispatchers.IO) {
try {
val retroInstance =
RetroInstance.getRetroInstance(MainActivity.BaseUrl).create(RetroService::class.java)
val response = retroInstance.getDataFromApi(1, Cmd.WIFIAPP_CMD_FILELIST)
recyclerListLiveData.postValue(response)
} catch (e: Exception) {
var response: ResponseData? = null
when (e) {
is ConnectException -> {
recyclerListLiveData.postValue(response)
}
is SocketTimeoutException -> {
recyclerListLiveData.postValue(response)
}
}
}
}
}
}
Service to make a command (processing results in the Handler):
class DeviceService {
private val handler: Handler
private var mJob: Job? = null
constructor(handler: Handler) {
this.handler = handler
}
fun sendCommand(cmd: Int) {
val service = RetroInstance.buildService(MainActivity.BaseUrl, RetroService::class.java)
mJob = CoroutineScope(Dispatchers.IO).launch {
val response = when (cmd) {
Cmd.WIFIAPP_CMD_MOVIE_GET_LIVEVIEW_FMT -> {
try {
service.getLinkFromApi(1, cmd)
} catch (e: Exception) {
handler.obtainMessage(Msg.MESSAGE_TOAST, "Error in getting data").sendToTarget()
mJob?.cancel()
}
}
else -> {
try {
service.makeCommand(1, cmd)
} catch (e: Exception) {
handler.obtainMessage(Msg.MESSAGE_TOAST, "Error in getting data").sendToTarget()
mJob?.cancel()
}
}
}
withContext(Dispatchers.Main) {
try {
when (cmd) {
Cmd.WIFIAPP_CMD_MOVIE_GET_LIVEVIEW_FMT -> {
handler.obtainMessage(Msg.MESSAGE_LINK_FORMAT, response).sendToTarget()
}
else -> {
handler.obtainMessage(Msg.MESSAGE_PAR_FUNCTION, response).sendToTarget()
}
}
} catch (e: Exception) {
when (e) {
is ConnectException -> {
handler.obtainMessage(Msg.MESSAGE_TOAST, "Connection lost").sendToTarget()
}
is SocketTimeoutException -> {
handler.obtainMessage(Msg.MESSAGE_TOAST, "Connection lost").sendToTarget()
}
}
}
mJob?.cancelAndJoin()
}
}
}
}
Downloading a images:
class ThumbDownloader(dataAdapter: DeviceAdapter, data: ArrayList<MediaData>, file_path: String, swipe: SwipeRefreshLayout) : CoroutineScope {
private var job: Job = Job()
private var file_path: String
private var dataAdapter: DeviceAdapter
private var data: ArrayList<MediaData>
private var swipe: SwipeRefreshLayout
init {
this.data = data
this.file_path = file_path
this.dataAdapter = dataAdapter
this.swipe = swipe
}
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
fun cancel() {
job.cancel()
}
fun execute() = async {
var item: File? = null
for (i in data) {
val task = async(Dispatchers.IO) {
val url = i.fPath!!
val real_url = "${MainActivity.BaseUrl}$url"
item = NetworkUtil.downloadFile(real_url, file_path, false)
}
task.await()
if (item != null) {
dataAdapter.insertItem(i)
}
}
cancel()
swipe.isRefreshing = false
}
}
Any ideas how to come up with their synchronization while waiting for the previous commands to complete?
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