access token and refresh token from oneTap sign in/up - android

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.

Related

android parallel API requests with retrofit and coroutines

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

Firebase Auth with Kotlin Flow

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 {
[...]
}

How do I upload image file to google drive from android

I'm having a issue uploading a file to Google Drive.
W/System.err: java.io.FileNotFoundException: /storage/emulated/0/Android/data/dev/files/backup/https:/i.picsum.photos/id/723/200/200.jpg? (No such file or directory)
This is my following code.
Source code from
https://www.section.io/engineering-education/backup-services-with-google-drive-api-in-android/
class GoogleDriveUtils(private val context: Context) {
fun initializeGoogleClient() {
val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.requestScopes(Scope(DriveScopes.DRIVE_FILE), Scope(DriveScopes.DRIVE_APPDATA))
.build()
val client = GoogleSignIn.getClient(context, signInOptions)
(context as? Activity)?.startActivityForResult(
client.signInIntent,
SignUpFragment.RC_SIGN_IN
)
}
fun googleDriverIntegrate() {
GoogleSignIn.getLastSignedInAccount(context)?.let { googleAccount ->
val credential = GoogleAccountCredential.usingOAuth2(
context, listOf(DriveScopes.DRIVE_FILE)
)
credential.selectedAccount = googleAccount.account!!
}
}
fun getDriveService(): Drive? {
GoogleSignIn.getLastSignedInAccount(context)?.let { googleAccount ->
val credential = GoogleAccountCredential.usingOAuth2(
context, listOf(DriveScopes.DRIVE_FILE)
)
credential.selectedAccount = googleAccount.account!!
return Drive.Builder(
AndroidHttp.newCompatibleTransport(),
JacksonFactory.getDefaultInstance(),
credential
)
.setApplicationName(context.getString(R.string.app_name))
.build()
}
return null
}
fun accessDriveFiles(savePath: String?) {
getDriveService()?.let { googleDriveService ->
CoroutineScope(Dispatchers.IO).launch {
var pageToken: String?
do {
val result = googleDriveService.files().list().apply {
spaces = "drive"
fields = "nextPageToken, files(id, name)"
pageToken = this.pageToken
}.execute()
result.files.forEach { file ->
Log.d("FILE", ("name=${file.name} id=${file.id}"))
}
} while (pageToken != null)
}
val FILE_NAME_BACKUP="test.txt"
val localFileDirectory = File(context.getExternalFilesDir("backup")!!.toURI())
val actualFile = savePath?.let { File(it) }
val gFile = com.google.api.services.drive.model.File()
if (actualFile != null) {
gFile.name = actualFile.name
}
if (actualFile != null) {
Log.e("actualFile", actualFile.name)
}
val fileContent = FileContent("text/plain", actualFile)
googleDriveService.Files().create(gFile, fileContent).execute()
}
}
fun uploadFileToGDrive(path: String?) {
getDriveService()?.let { googleDriveService ->
CoroutineScope(Dispatchers.IO).launch {
try {
val localFileDirectory =
File(context.getExternalFilesDir("backup")!!.toURI())
val actualFile = File("${localFileDirectory}/$path")
val gFile = com.google.api.services.drive.model.File()
gFile.name = actualFile.name
val fileContent = FileContent("image/jpeg", actualFile)
googleDriveService.Files().create(gFile, fileContent).execute()
Log.e("File uploaded", "File uploaded")
} catch (exception: Exception) {
exception.printStackTrace()
}
}
} ?: Toast.makeText(context, "Please Log In first!", LENGTH_SHORT).show()
}
fun downloadFileFromGDrive(id: String) {
getDriveService()?.let { googleDriveService ->
CoroutineScope(Dispatchers.IO).launch {
googleDriveService.Files().get(id).execute()
}
} ?: Toast.makeText(context, "Please Log In first!", LENGTH_SHORT).show()
}
}

Android paging from local data (without room)

I'm trying to work with pagination library, to get list from server, and use it with local data, but i don't want to use room for it (don't have db in my app, and don't want to add it just for it),
so i have mediator, and i'm trying to implement PagingSource. the list should be flowable, so when i delete an item, it will update automatically.
mediator
class EventMediator(
private val id: String,
private val remoteDataSource: EventRemote,
private val eventLocalData: EvrntsLocal
) : RemoteMediator<Int, EventItem>() {
var hasNextKey = true
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, EventItem>
): MediatorResult {
try {
val loadKey = when (loadType) {
LoadType.REFRESH -> STARTING_MEAL_INDEX
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
if (!eventLocalData.hasNextKey) {
return MediatorResult.Success(endOfPaginationReached = true)
}
eventLocalData.getNumOfMeals()
}
}
val response = remoteDataSource.getEvents(loadKey)
return if (response is Result.Success) {
hasNextKey = !response.data.lastPage
if (loadType == LoadType.REFRESH) {
eventLocalData.clearMeals()
}
eventLocalData.saveMeals(response.data.items)
MediatorResult.Success(endOfPaginationReached = !hasNextKey)
} else {
MediatorResult.Error(IOException("Failed to get Events"))
}
} catch (e: IOException) {
return MediatorResult.Error(e)
} catch (e: HttpException) {
return MediatorResult.Error(e)
}
}
}
EventSource:
class EventSource(
private val eventLocalData: EvrntsLocal
) : PagingSource<Int, EventItem>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, EventItem> {
val offset = (params.key ?: STARTING_MEAL_INDEX)
return try {
val response = eventLocalData.getMeals()
LoadResult.Page(
data = response,
prevKey = if (offset - NUM_OF_EVENTS <= STARTING_MEAL_INDEX) null else offset - NUM_OF_EVENTS,
nextKey = if (offset + NUM_OF_EVENTS >= response.size) null else offset + NUM_OF_EVENTS
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
} catch (exception: HttpException) {
return LoadResult.Error(exception)
}
}
}
repository
fun getEvents(folderId: String): Flow<PagingData<EventItem>> {
return Pager(
config = PagingConfig(50),
remoteMediator = EventMediator(folderId, remoteDataSource, localDataSource),
pagingSourceFactory = { EventSource(localDataSource) }
) .flow
}
my local data:
class EvrntsLocal #Inject constructor(
) {
private val _eventChannel = ConflatedBroadcastChannel<List<EventItem>>(emptyList())
var hasNextKey: Boolean = true
fun observeMeals(): Flow<List<EventItem>> {
return _eventChannel.asFlow()
}
fun getMeals(): List<EventItem> {
return _eventChannel.value
}
fun saveMeals(list: List<EventItem>) {
_eventChannel.offer(_eventChannel.value.plus(list))
}
fun getNumOfMeals(): Int {
return _eventChannel.value.size
}
fun clearMeals() {
_eventChannel.offer(emptyList())
}
}

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

Categories

Resources