my login is not working ..im using retrofit and viewmodel ...actually scenerio is onclick of login shows the transition(loginpage to loginpage) but it is not moving to loginpage to homepage....
this method model.ResponseData.observeis not getting call
i dont know where im going wrong
need help thanks
Loginactivity:--
class LoginActivity : AppCompatActivity() {
lateinit var model: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.login_activity)
val button = findViewById<ImageView>(R.id.plusbutton)
val forgotpassword=findViewById<TextView>(R.id.forgotpassword)
button.setOnClickListener {
val i = Intent(applicationContext, RegisterActivity::class.java)
startActivity(i)
}
forgotpassword.setOnClickListener{
val i = Intent(applicationContext, ForgotPassword::class.java)
startActivity(i)
}
model = ViewModelProvider(this)[LoginViewModel::class.java]
model.ResponseData.observe(this, object : Observer<LoginResponse?> {
override fun onChanged(t: LoginResponse?) {
val intent = Intent(applicationContext, HomeActivity::class.java)
intent.flags =
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish()
}
})
loginbtn.setOnClickListener {
val email = loginuser.text.toString().trim()
val password = loginpassword.text.toString().trim()
if (email.isEmpty()) {
Toast.makeText(
applicationContext, "Data is missing", Toast.LENGTH_LONG
).show()
loginuser.error = "Email required"
loginuser.requestFocus()
return#setOnClickListener
} else if (password.isEmpty()) {
loginpassword.error = "Password required"
loginpassword.requestFocus()
return#setOnClickListener
}
else {
model.loadAccountData(email,password)
}
}
}}
viewmodel:--
class LoginViewModel(context: Application,private val savedStateHandle: SavedStateHandle) : AndroidViewModel(context) {
private var _aResponseData = MutableLiveData<LoginResponse?>()
val user: MutableLiveData<String> = savedStateHandle.getLiveData("user", "")
val password: MutableLiveData<String> = savedStateHandle.getLiveData("password", "")
val ResponseData: MutableLiveData<LoginResponse?>
get() {
if (_aResponseData == null) {
_aResponseData = MutableLiveData<LoginResponse?>()
loadAccountData(user.toString(), password.toString())
}
return _aResponseData
}
fun loadAccountData(email:String, password:String) {
RetrofitClient.instance.userLogin(email, password)
.enqueue(object : Callback<LoginResponse> {
override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
Log.d("res", "" + t)
_aResponseData.value = null
}
override fun onResponse(
call: Call<LoginResponse>,
response: Response<LoginResponse>
) {
var res = response
if (res.body()?.status == 200) {
_aResponseData.value = response.body()
} else {
try {
val jObjError =
JSONObject(response.errorBody()!!.string())
Toast.makeText(
getApplication(),
jObjError.getString("user_msg"),
Toast.LENGTH_LONG
).show()
} catch (e: Exception) {
Log.e("errorrr", e.message)
}
}
}
})
}
}
After discussion and debugging, looks like issue was not on observer but actually during parsing of response from API call via retrofit.
_aResponseData.value = response.body()
Here response.body() was not able to parse response as LoginResponse class object which was eventually causing issue further more on navigation.
Fixing issue on parsing helps O.P. through debugging on main concern.
Try this
if (res.body()?.status == 200) {
_aResponseData.postValue(response.body())
}
Related
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 {
[...]
}
I was trying to get response from viewModel But, I am having hard time to use the response data on another activity to display it on a text view.
I have already setup the backend for the repo's and interfaces, data classes etc...
Thank you!!!
// View Model
class HomeActivityViewModel : ViewModel() {
lateinit var createPostLiveData: MutableLiveData<PostResponseData?>
init {
createPostLiveData = MutableLiveData()
}
fun getPostLiveObserver(): MutableLiveData<PostResponseData?> {
return createPostLiveData
}
fun createPostData(postdata: PostData) {
val retroService = RetrofitApiFactory.retroInstance().create(ChefApi::class.java)
val call = retroService.postData(postdata)
call.enqueue(object : Callback<PostResponseData> {
override fun onResponse(
call: Call<PostResponseData>,
response: Response<PostResponseData>
) {
if (response.isSuccessful) {
createPostLiveData.postValue(response.body())
var text = response.body()!!.choices[0].text
Log.d("response", text) // only shows the one in the viewModel
} else {
createPostLiveData.postValue(null)
Log.d("failed", response.errorBody().toString())
}
}
override fun onFailure(call: Call<PostResponseData>, t: Throwable) {
Log.d("failed", t.message.toString())
createPostLiveData.postValue(null)
}
})
}
}
Activity.kt
class HomeActivity : AppCompatActivity() {
lateinit var mAuth: FirebaseAuth
lateinit var viewModel: HomeActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home_actvity)
mAuth = FirebaseAuth.getInstance()
initViewModel()
generate.setOnClickListener {
createPost()
}
logout.setOnClickListener {
logoutUser()
}
}
private fun createPost() {
// creating a post
val prompt = "Some string..."
val postdata = PostData(120, prompt, 0.3, 1.0, 0.0)
viewModel.createPostData(postdata)
}
private fun initViewModel() {
// initialize view model
viewModel = ViewModelProvider(this).get(HomeActivityViewModel::class.java)
viewModel.getPostLiveObserver().observe(this, Observer<PostResponseData?> {
if (it == null) {
Toast.makeText(this#HomeActivity, "Failed to post data", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this#HomeActivity, "Successfully posted data", Toast.LENGTH_SHORT)
.show()
}
})
}
private fun logoutUser() {
mAuth.signOut()
updateUI()
}
private fun updateUI() {
val intent = Intent(this#HomeActivity, MainActivity::class.java)
startActivity(intent)
finish()
}
Try to change your HomeActivityViewModel class and add a LiveData object to it:
class HomeActivityViewModel : ViewModel() {
var _createPostLiveData: MutableLiveData<PostResponseData?>()
// Live data instance
val createPostLiveData
get() = _createPostLiveData
fun createPostData(postdata: PostData) {
val retroService = RetrofitApiFactory.retroInstance().create(ChefApi::class.java)
val call = retroService.postData(postdata)
call.enqueue(object : Callback<PostResponseData> {
override fun onResponse(
call: Call<PostResponseData>,
response: Response<PostResponseData>
) {
if (response.isSuccessful) {
// Update live data value
_createPostLiveData.value = response.body()
var text = response.body()!!.choices[0].text
Log.d("response", text) // only shows the one in the viewModel
} else {
// Update live data value
_createPostLiveData.value = null
Log.d("failed", response.errorBody().toString())
}
}
override fun onFailure(call: Call<PostResponseData>, t: Throwable) {
Log.d("failed", t.message.toString())
// Update live data value
_createPostLiveData.value = null
}
})
}
}
You should then be able to observe the LiveData instance in your Activity:
class HomeActivity : AppCompatActivity() {
lateinit var mAuth: FirebaseAuth
// Initialize view model in declaration
private val viewModel: HomeActivityViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home_actvity)
mAuth = FirebaseAuth.getInstance()
// Observe forĀ `createPostLiveData` changes
viewModel.createPostLiveData.observe(this) {
if (it == null) {
Toast.makeText(this#HomeActivity, "Failed to post data", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this#HomeActivity, "Successfully posted data", Toast.LENGTH_SHORT)
.show()
}
}
generate.setOnClickListener {
createPost()
}
logout.setOnClickListener {
logoutUser()
}
}
private fun createPost() {
// creating a post
val prompt = "Some string..."
val postdata = PostData(120, prompt, 0.3, 1.0, 0.0)
viewModel.createPostData(postdata)
}
...
I am calling the API using retrofit in ApiClient Class. Where I am trying to store the output of successful login either message or responseBody into the output string. I tried using the output string as part of ViewModel Observer type too
But I CANNOT pass the value of output from AplClient to MainActivity
APICLIENT
...
package com.example.services.api
lateinit var service: ApiInterface
object ApiClient {
#JvmStatic
private val BASE_URL = GlobalConstants.SWAGGER
//private val sharedPrefClass = SharedPrefClass()
#JvmStatic
private var mApiInterface: ApiInterface? = null
var output = "Initializing"
// val sharedPrefClass: SharedPrefClass? = null
// #JvmStatic
// fun getApiInterface(): ApiInterface {
// return setApiInterface()
// }
#JvmStatic
fun setApiInterface() : String {
val platform = GlobalConstants.PLATFORM
var mAuthToken = GlobalConstants.SESSION_TOKEN
var companyId = GlobalConstants.COMPANY_ID
var phone = phone
var cCode = cCode
//Here a logging interceptor is created
//Here a logging interceptor is created
val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
//The logging interceptor will be added to the Http client
//The logging interceptor will be added to the http client
val httpClient = OkHttpClient.Builder()
httpClient.addInterceptor(logging)
//The Retrofit builder will have the client attached, in order to get connection logs
//The Retrofit builder will have the client attached, in order to get connection logs
val retrofit: Retrofit = Retrofit.Builder()
.client(httpClient.build())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.build()
service = retrofit.create<ApiInterface>(ApiInterface::class.java)
val call: Call<Tree> = service.post(phone, cCode, companyId, platform)
var model: Model
call.enqueue(object : Callback<Tree> {
override fun onResponse(
call: Call<Tree>, response: Response<Tree>)
{
Log.e(TAG, "Success")
if (!response.isSuccessful()) {
model = Model("Success", "91", "8884340404")
model.output += response.body().toString()
return;
}
}
override fun onFailure(
call: Call<Tree>,
t: Throwable
) {
Log.e(TAG, "Json/Network Error")
model = Model("Json/Network Error", "91", "8884340404")
model.output = "Json/Network Error"
// handle execution failures like no internet connectivity
}
})
return output
}
}
VIEWMODEL
package com.example.kotlinloginapi.viewModel
import androidx.databinding.Observable
import androidx.databinding.ObservableField
import androidx.lifecycle.MutableLiveData
class Model {
var output:String? = null
var cCode:String? = null
var phone: String? = null
constructor(output: String?, cCode: String?, phone: String?) {
this.output = output
this.cCode = cCode
this.phone = phone
}
}
...
MAINACTIVTY
...
package com.example.kotlinloginapi.ui.login
lateinit var textView: TextView
class MainActivity : AppCompatActivity() {
lateinit var mainBinding : ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
textView = findViewById<TextView>(R.id.textView)
val mApiService: ApiService<Tree>
val mApiClient = ApiClient
var model: Model
model = Model("Creating View", "91", "8884340404")
model.output = "Creating View"
Observable.just(setApiInterface())
.subscribe {
println(it)
textView!!.append(setApiInterface())}
// mainBinding.viewModel!!.output = "Creating View"
textView.append("\n" + model.output)
/*fun getLoginData(jsonObject : JsonObject?) {
if (jsonObject != null) {
val mApiService = ApiService<JsonObject>()
mApiService.get(
object : ApiResponse<JsonObject> {
override fun onResponse(mResponse : Response<JsonObject>) {
val loginResponse = if (mResponse.body() != null)
finish()
else {
output = mResponse.body().toString()
}
}
override fun onError(mKey : String) {
Toast.makeText(
applicationContext,
"Error",
Toast.LENGTH_LONG
).show()
}
}, ApiClient.getApiInterface().callLogin(jsonObject)
)
}
}*/
}
}
LOGINACTIVITY
package com.example.kotlinloginapi.ui.login
lateinit var cCode: String
lateinit var phone: String
class LoginActivity : AppCompatActivity() {
lateinit var loginBinding : ActivityLoginBinding
lateinit var eTcCode: EditText
lateinit var eTphone: EditText
private lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loginBinding = DataBindingUtil.setContentView(this, R.layout.activity_login)
eTcCode = findViewById<EditText>(R.id.etcCode)
eTphone = findViewById<EditText>(R.id.etPhone)
val login = findViewById<Button>(R.id.login)
val loading = findViewById<ProgressBar>(R.id.loading)
var model = Model("Binding Model", "91", "8884340404")
loginBinding.model = model
loginBinding.lifecycleOwner
loginViewModel = ViewModelProviders.of(this, LoginViewModelFactory())
.get(LoginViewModel::class.java)
loginViewModel.loginFormState.observe(this#LoginActivity, Observer {
val loginState = it ?: return#Observer
// disable login button unless both username / password is valid
login.isEnabled = loginState.isDataValid
if (loginState.usernameError != null) {
eTcCode.error = getString(loginState.usernameError)
}
if (loginState.passwordError != null) {
eTphone.error = getString(loginState.passwordError)
}
})
loginViewModel.loginResult.observe(this#LoginActivity, Observer {
val loginResult = it ?: return#Observer
loading.visibility = View.VISIBLE
if (loginResult.error != null) {
showLoginFailed(loginResult.error)
}
if (loginResult.success != null) {
updateUiWithUser(loginResult.success)
}
setResult(Activity.RESULT_OK)
//Complete and destroy login activity once successful
finish()
})
eTcCode.afterTextChanged {
loginViewModel.loginDataChanged(
eTcCode.text.toString(),
eTphone.text.toString()
)
}
eTphone.apply {
afterTextChanged {
loginViewModel.loginDataChanged(
eTcCode.text.toString(),
eTphone.text.toString()
)
}
setOnEditorActionListener { _, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_DONE ->
loginViewModel.login(
eTcCode.text.toString(),
eTphone.text.toString()
)
}
false
}
login.setOnClickListener {
loading.visibility = View.VISIBLE
loginViewModel.login(eTcCode.text.toString(), eTphone.text.toString())
}
}
}
private fun updateUiWithUser(model: LoggedInUserView) {
val welcome = getString(R.string.welcome)
val displayName = model.displayName
// TODO : initiate successful logged in experience
cCode = eTcCode.text.toString()
phone = eTphone.text.toString()
var intent = Intent(this, MainActivity::class.java)
startActivity(intent)
Toast.makeText(
applicationContext,
"$welcome $displayName",
Toast.LENGTH_LONG
).show()
}
private fun showLoginFailed(#StringRes errorString: Int) {
Toast.makeText(applicationContext, errorString, Toast.LENGTH_SHORT).show()
}
}
/**
* Extension function to simplify setting an afterTextChanged action to EditText components.
*/
fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) {
this.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(editable: Editable?) {
afterTextChanged.invoke(editable.toString())
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
})
}
...
Ideally, I want to use binding observers but I am unable to pass the output of retrofit to MainActivity even without that.
I moved the enqueue call to main activity which solved the problem. Now I can call text views. Will try to achieve this without moving and live data in future.
In my main activity i have a function that runs once the login button it pressed. I'm calling a class that attempts to login via an API which takes a second or two to run. However, when i'm calling the login class it seems to be threaded and doesn't wait for the login to complete and returns false which is the default. Example code is as follows:
class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun loginBtnClicked(view: View) {
progressBar.visibility = View.VISIBLE
// get domain info
val domain: TextView = findViewById<TextView>(R.id.loginDomain)
val domainUrl = domain.text.toString()
val url = "$domainUrl/api/"
// get username info
val username = loginUsername
// get password info
val password = loginPassword
if (ApiGet(
url = url,
username = username.text.toString(),
password = password.text.toString()
).login()) {
println("It worked")
val dashboard = Intent(this, DashboardActivity::class.java)
Consts.DOMAIN = url
Consts.USERNAME = username.text.toString()
Consts.PASSWORD = password.text.toString()
startActivity(dashboard)
} else {
println("It didn't work")
progressBar.visibility = View.INVISIBLE
runOnUiThread {
Log.i(ContentValues.TAG, "runOnUiThread")
Toast.makeText(
applicationContext,
"Please check the domain, username and password then try again.",
Toast.LENGTH_SHORT
).show()
}
}
}
}
class ApiGet(val url: String, val username: String = Consts.USERNAME, val password: String = Consts.PASSWORD) {
fun login(): Boolean {
var loginAttempt: Boolean = false
var apiData: ApiLogin
val creds = Credentials.basic(username, password)
val request = Request.Builder().url(url).header("Authorization", creds).build()
val client = OkHttpClient()
client.newCall(request).enqueue(
object : Callback {
override fun onResponse(call: Call, response: Response) {
val body: String? = response.body()?.string()
val gson: Gson = GsonBuilder().create()
apiData = gson.fromJson(body, ApiLogin::class.java)
if (apiData.detail == "Invalid username/password.") {
loginAttempt = false
println(loginAttempt)
} else {
loginAttempt = true
println(loginAttempt)
}
}
override fun onFailure(call: Call, e: IOException) {
Toast.makeText(
MainActivity().getApplicationContext(),
"Please check your connection and try again.",
Toast.LENGTH_SHORT
).show()
loginAttempt = false
}
})
return loginAttempt
}
class ApiLogin(val detail: String)
}
It is because your newCall method runs async to the main thread meaning the rest of your code keeps running after you call it while it waits on another thread. To fix this rather than returning your result you can handle it in a callback like so:
class ApiGet(val url: String, val username: String = Consts.USERNAME, val password: String = Consts.PASSWORD) {
fun login(completion: (Boolean)->Unit) {
var loginAttempt: Boolean = false
var apiData: ApiLogin
val creds = Credentials.basic(username, password)
val request = Request.Builder().url(url).header("Authorization", creds).build()
val client = OkHttpClient()
client.newCall(request).enqueue(
object : Callback {
override fun onResponse(call: Call, response: Response) {
val body: String? = response.body()?.string()
val gson: Gson = GsonBuilder().create()
apiData = gson.fromJson(body, ApiLogin::class.java)
if (apiData.detail == "Invalid username/password.") {
println(loginAttempt)
completion(False)
} else {
print(loginAttempt)
completion(True)
}
}
override fun onFailure(call: Call, e: IOException) {
Toast.makeText(
MainActivity().getApplicationContext(),
"Please check your connection and try again.",
Toast.LENGTH_SHORT
).show()
completion(False) }
})
}
You can call the login function like so:
ApiGet(url = url,
username = username.text.toString(),
password = password.text.toString()).login { result ->
if (result) {
// success
} else {
// failure
}
}
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