I am building an android app in MVVM architecture using Firebase. I am trying to do User's password change and whenever i start my code, application freezes or just stops responding. I spent a lot of time to search what is wrong with it and yet no fix. If anyone know why it behave like this I would appreciate your help. My code:
Function called in fragment:
private fun startChangePasswordDialog(){
val dialogView = LayoutInflater.from(activity).inflate(R.layout.dialog_change_password, null)
val builder = AlertDialog.Builder(activity).setView(dialogView)
val dialog: AlertDialog = builder.show()
val changePassword = dialogView.findViewById<Button>(R.id.changePasswordBT)
val cancel = dialogView.findViewById<Button>(R.id.changePasswordCancelBT)
val passwordET = dialogView.findViewById<EditText>(R.id.changePasswordET)
changePassword?.setOnClickListener {
val newPassword = passwordET.text.trim().toString()
if (TextUtils.isEmpty(newPassword) || newPassword.length < viewModel.PASSWORD_MIN_VALUE){
Toast.makeText(requireContext(), R.string.password_too_short, Toast.LENGTH_SHORT).show()
}
else{
viewModel.changeUsersPassword(newPassword)
viewModel.needUserAuthentication.observe(requireActivity(), {
if (it == true) reAuthenticateUser()
})
}
dialog.dismiss()
}
cancel?.setOnClickListener {
dialog.dismiss()
}
ViewModel function:
fun changeUsersPassword(password: String) {
Log.d(TAG,"Starting user's password change procedure")
when (repository.changeUserPassword(password)){
PasswordChangeCallbackEnum.FACEBOOK_USER -> {
_toastMessage.value = R.string.facebook_user_password_change
Log.d(TAG, "User's password will not be changed, logged in as Facebook user")
}
PasswordChangeCallbackEnum.PASSWORD_CHANGE_ERROR -> {
_toastMessage.value = R.string.password_change_error
Log.d(TAG, "Error while changing user's password")
}
PasswordChangeCallbackEnum.PASSWORD_CHANGED -> {
_toastMessage.value = R.string.password_change_success
Log.d(TAG, "User's password changed successfully")
}
PasswordChangeCallbackEnum.NEED_USER_AUTHENTICATION -> {
_needUserAuthentication.value = true
}
}
}
Firebase Repository (I have changed it several times when tried to fix this):
fun changeUserPassword(password: String): PasswordChangeCallbackEnum {
var result = PasswordChangeCallbackEnum.PASSWORD_CHANGE_ERROR
if (currentUser != null) {
for (userInfo in currentUser.providerData) {
if (userInfo.providerId == "facebook.com") {
Log.d(TAG, "Cannot change password for user logged in with facebook")
result = PasswordChangeCallbackEnum.FACEBOOK_USER
}
}
}
try{
val updateTask = authentication.currentUser?.updatePassword(password)
updateTask?.addOnSuccessListener {
Log.d(TAG, "User's password change state: SUCCESS")
result = PasswordChangeCallbackEnum.PASSWORD_CHANGED
}
}catch (exception: FirebaseAuthRecentLoginRequiredException){
Log.d(TAG, "Need user to authenticate again")
result = PasswordChangeCallbackEnum.NEED_USER_AUTHENTICATION
}
return result
}
The problem is, you are doing task in ui thread.
Use coroutines for the task to do in worker thread .
you can have more information about coroutines. here
You can also use RxJava for it or some Async task.
It will prevent the ui freezing
Related
I have sophisticated scenario where a set of mutually dependent coroutine flows depends on each other and chained:
viewModelScope.launch {
repository.cacheAccount(person)
.flatMapConcat { it->
Log.d(App.TAG, "[2] create account call (server)")
repository.createAccount(person)
}
.flatMapConcat { it ->
if (it is Response.Data) {
repository.cacheAccount(it.data)
.collect { it ->
// no op, just execute the command
Log.d(App.TAG, "account has been cached")
}
}
flow {
emit(it)
}
}
.catch { e ->
Log.d(App.TAG, "[3] get an exception in catch block")
Log.e(App.TAG, "Got an exception during network call", e)
state.update { state ->
val errors = state.errors + getErrorMessage(PersonRepository.Response.Error.Exception(e))
state.copy(errors = errors, isLoading = false)
}
}
.collect { it ->
Log.d(App.TAG, "[4] collect the result")
updateStateProfile(it)
}
}
cache an account on the local disk
create an account on the backend
in positive scenario, cache the newly create account in the local disk
Now I have to add more calls to a new API endpoint and the scenario become even more sophisticated. This endpoint is a ethereum chain.
4a. In the positive scenario, put in the local disk (cache) initiated transaction cacheRepository.createChainTx()
4b. In the negative scenario, just emit further the response from the backend
4a.->5. Register user on the 2nd endpoint repository.registerUser()
The response from 2nd endpoint put in the cache by updating existing row. Even negative case except of exception should be cached to update status of tx.
viewModelScope.launch {
lateinit var newTx: ITransaction
cacheRepository.createChainTxAsFlow(RegisterUserTransaction(userWalletAddress = userWalletAddress))
.map { it ->
newTx= it
repository.registerUserOnSwapMarket(userWalletAddress)
}
.onEach { it -> preProcessResponse(it, newTx) }
.flowOn(backgroundDispatcher)
.collect { it -> processResponse(it) }
}
This a scenario which should be integrated into the 1st Flow chain.
The issue is I do not see how to do it clear in Flow chain. I can rewrite code without chaining, but it also bring variety if else statements.
How would you do this scenario in human readable way?
I'll ended up with this code for transition period:
viewModelScope.launch(backgroundDispatcher) {
try {
var cachedPersonProfile = repository.cacheAccount(person)
var createAccountResponse = repository.createAccount(person)
when(createAccountResponse) {
is Response.Data -> {
repository.cacheAccount(createAccountResponse.data)
val cachedTx = cacheRepository.createChainTx(RegisterUserTransaction(userWalletAddress = person.userWalletAddress))
val chainTx = walletRepository.registerUserOnSwapMarket(userWalletAddress = person.userWalletAddress)
when(chainTx) {
is ru.home.swap.core.network.Response.Data -> {
if (chainTx.data.isStatusOK()) {
cachedTx.status = TxStatus.TX_MINED
} else {
cachedTx.status = TxStatus.TX_REVERTED
}
}
is ru.home.swap.core.network.Response.Error.Message -> {
cachedTx.status = TxStatus.TX_EXCEPTION
}
is ru.home.swap.core.network.Response.Error.Exception -> {
cachedTx.status = TxStatus.TX_EXCEPTION
}
}
cacheRepository.createChainTx(cachedTx)
withContext(Dispatchers.Main) {
state.update { state ->
if (cachedTx.status == TxStatus.TX_MINED) {
state.copy(
isLoading = false,
profile = createAccountResponse.data,
status = StateFlagV2.PROFILE
)
} else {
val txError = "Failed register the profile on chain with status ${TxStatus.TX_MINED}"
state.copy(
isLoading = false,
errors = state.errors + txError
)
}
}
}
}
else -> { updateStateProfile(createAccountResponse) }
}
} catch (ex: Exception) {
withContext(Dispatchers.Main) {
state.update { state ->
val errors = state.errors + getErrorMessage(PersonRepository.Response.Error.Exception(ex))
state.copy(errors = errors, isLoading = false)
}
}
}
}
If you have a better alternative, please share it in the post as an answer.
I have AlertDialog with a custom layout to sign in users, which check if it is user's first time signing in by checking the Firestore for the existence of their email.
private fun showSignInDialog() {
val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext())
builder.setTitle("Sign In")
val inflater = requireActivity().layoutInflater
val dialogView = inflater.inflate(R.layout.dialog_sign_in, null)
builder.setView(dialogView)
val editEmail = dialogView.findViewById<EditText>(R.id.edit_email)
val editPassword = dialogView.findViewById<EditText>(R.id.edit_password)
builder.setPositiveButton("OK") { _, _ ->
val email = editEmail.text.toString()
val password = editPassword.text.toString()
signInWithEmail(email, password)
}
builder.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
builder.create().show()
}
private fun signInWithEmail(email: String, password: String) {
auth = Firebase.auth
auth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(requireActivity()) { task ->
if (task.isSuccessful) {
Log.d(TAG, "signInWithEmail:success")
val signedInEmail = auth.currentUser?.email
firestoreService = FirestoreService()
firestoreService.doesUserAlreadyExists(object : UserCallback {
override fun onCallback(dRef: String) {
Log.d(TAG, "dRef = $dRef")
if (dRef.isEmpty())
findNavController().navigate(R.id.action_signIn_to_ceateProfile)
else
findNavController().navigate(R.id.action_signIn_to_main)
}
}, signedInEmail!!)
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "signInWithEmail:failure", task.exception)
Toast.makeText(
requireContext(), "Authentication failed.",
Toast.LENGTH_SHORT
).show()
}
}
}
FirestoreService.kt
class FirestoreService {
private val db = Firebase.firestore
fun doesUserAlreadyExists(callback: UserCallback, email: String) {
db.collection("Users")
.whereEqualTo("email", email).get()
.addOnSuccessListener { documents ->
for (document in documents) {
val docRef = document.id
callback.onCallback(docRef)
Log.d(TAG, "docRef = $docRef")
}
}
.addOnFailureListener { e ->
Log.w(TAG, "Error adding User document", e)
}
}
}
But this doesUserAlreadyExists never get executed, this is probably because since this method is async and there is nothing to return to after the finish of async callback, since dialog is destroyed by that time. How could I make this work?
Update :
I tried same logic on fragments instead of dialogs, same outcome, It seems something have to do with the firestoreService.doesUserAlreadyExists inside the addOnCompleteListener since one async callback is inside an another async callback. How to fix this?
Issue was there was no "Users" collection to check since I have deleted the "Users" collection so neither addOnSuccessListener or addOnFailureListener was called.
I wish there was an exception being thrown when a collection does not exist.
It is always to better to use addOnCompleteListener over addOnSuccessListener listener since it is guaranteed to be triggered no matter what
ex:
db.collection(USER_COLLECTION)
.whereEqualTo("email", email).get()
.addOnCompleteListener { task ->
if(task.isSuccessful){
for (document in task.result) {
if (document.exists()) {
//..
}
}
}
else{
//..
}
}
I don't know how to make my failure toast message to show only once.
Toast.makeText(this, vm.logInResult.value, Toast.LENGTH_SHORT).show()
private fun addData(edtTxt: String, pasTxt: String) {
val repository = UserRepository()
val viewModelFactory = UserViewModelFactory(repository)
viewModel = ViewModelProvider(this, viewModelFactory).get(UserViewModel::class.java)
viewModel.pushUser(edtTxt, pasTxt)
viewModel.userPush.observe(this, Observer { response ->
if (response.isSuccessful) {
dismissLogoProgressDialog()
Log.d("MainResponse", response.body().toString())
Log.d("MainExecute", response.code().toString())
Log.d("Main", response.message())
val check = response.body()
Log.d("checkdata", "" + check?.userinfo?.email)
val tokn: String = check!!.token
if (sharedPreference.getValueString("token") != null) {
sharedPreference.clearSharedPreference()
}
sharedPreference.save("token", tokn)
sharedPreference.save("login_uid", check.userinfo.uid)
sharedPreference.save("change_pass", pasTxt)
println(check)
startActivity(Intent(this, DashboardActivity::class.java))
finish()
} else {
dismissLogoProgressDialog()
Toast.makeText(this, "Password mismatch", Toast.LENGTH_SHORT).show()
}
})
}
Are you sure you only call this Toast once? Or is this Toast created in a loop? In that case; you need to breakout of the loop first.
The function may have been placed within a loop and the else clause is may always be taken.
Are the Log functions printing anything to the console?
Is there anyway you could edit the question and show us where this function is called?
Situation
I submit data setTripDeliver, the collect works fine (trigger LOADING and then SUCCESS). I pressed a button go to next fragment B (using replace). After that, I press back button (using popbackstack). the collect SUCCESS triggered.
Codes Related
These codes at the FragmentA.kt inside onViewCreated.
private fun startLifeCycle() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
collectTripDeliver()
}
launch {
collectTripReattempt()
}
}
}
}
These codes when to submit data at a button setOnClickListener.
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.setTripDeliver(
verificationCode,
remark
)
}
Method to collect flow collectTripReattempt()
private suspend fun collectTripReattempt() {
viewModel.tripReattempt.collect {
when (it) {
is Resource.Initialize -> {
}
is Resource.Loading -> {
Log.i("???","collectTripReattempt loading")
handleSaveEarly()
}
is Resource.Success -> {
val error = it.data?.error
if (error == null) {
Tools.showToast(requireContext(), "Success Reattempt")
Log.i("???","collectTripReattempt Success")
} else {
Tools.showToast(requireContext(), "$error")
}
handleSaveEnding()
}
is Resource.Error -> {
handleSaveEnding()
}
}
}
}
Below codes are from ViewModel.
private val _tripDeliver =
MutableStateFlow<Resource<TripDeliverResponse>>(Resource.Initialize())
val tripDeliver: StateFlow<Resource<TripDeliverResponse>> = _tripDeliver
This method to call repository.
suspend fun setTripDeliver(
verificationCode: String?,
remark: String?
) {
_tripDeliver.value = Resource.Loading()
try {
val result = withContext(ioDispatcher) {
val tripDeliverParameter = DeliverParameter(
verificationCode,
remark
)
val response = appRepository.setTripDeliver(tripDeliverParameter)
Resource.getResponse { response }
}
_tripDeliver.value = result
} catch (e: Exception) {
when (e) {
is IOException -> _tripDeliver.value =
Resource.Error(messageInt = R.string.no_internet_connection)
else -> _tripDeliver.value =
Resource.Error("Trip Deliver Error: " + e.message)
}
}
}
Logcat
2021-07-09 19:56:10.946 7446-7446/com.package.app I/???: collectTripReattempt loading
2021-07-09 19:56:11.172 7446-7446/com.package.app I/???: collectTripReattempt Success
2021-07-09 19:56:17.703 7446-7446/com.package.app I/???: collectTripReattempt Success
As you can see, the last Success is called again AFTER I pressed back button (popbackstack)
Question
How to make it trigger once only? Is it the way I implement it is wrong? Thank you in advance.
This is not problem of your implementation this is happening because of stateIn() which use used in your viewModel to convert regular flow into stateFlow
If according to your code snippet the success is triggered once again, then why not loading has triggered?
as per article, it is showing the latest cached value when you left the screen and came back you got the latest cached value on view.
Resource:
https://medium.com/androiddevelopers/migrating-from-livedata-to-kotlins-flow-379292f419fb
The latest value will still be cached so that when the user comes back to it, the view will have some data immediately.
I have found the solution, thanks to #Nurseyit Tursunkulov for giving me a clue. I have to use SharedFlow.
At the ViewModel, I replace the initialize with these:
private val _tripDeliver = MutableSharedFlow<Resource<TripDeliverResponse>>(replay = 0)
val tripDeliver: SharedFlow<Resource<TripDeliverResponse>> = _tripDeliver
At the replay I have to use 0, so this SharedFlow will trigger once. Next, change _tripDeliver.value to _tripDeliver.emit() like the codes below:
fun setTripDeliver(
verificationCode: String?,
remark: String?
) = viewModelScope.launch {
_tripDeliver.emit(Resource.Loading())
if (verificationCode == null && remark == null) {
_tripDeliver.emit(Resource.Error("Remark cannot be empty if verification is empty"))
return#launch
}
try {
val result = withContext(ioDispatcher) {
val tripDeliverParameter = DeliverParameter(
verificationCode,
remark,
)
val response = appRepository.setTripDeliver(tripDeliverParameter)
Resource.getResponse { response }
}
_tripDeliver.emit(result)
} catch (e: Exception) {
when (e) {
is IOException -> _tripDeliver.emit(Resource.Error(messageInt = R.string.no_internet_connection))
else -> _tripDeliver.emit(Resource.Error("Trip Deliver Error: " + e.message))
}
}
}
I hope this answer will help the others also.
I think this is because of coldFlow, you need a HotFlow. Another option is to try to hide and show fragment, instead of replacing. And yet another solution is to keep this code in viewModel.
In my opinion, I think your way of using coroutines in lifeScope is incorrect. After the lifeScope status of FragmentA is at Started again, the coroutine will be restarted:
launch {
collectTripDeliver()
}
launch {
collectTripReattempt()
}
So I think: You need to modify this way:
private fun startLifeCycle() {
viewLifecycleOwner.lifecycleScope.launch {
launch {
collectTripDeliver()
}
launch {
collectTripReattempt()
}
}
}
When the user wants to change his password, I prompt him with a Dialog to Re-authenticate. In this dialog, he can re-auth with password or via Google/Facebook. But when I update the firebase email, the user gets signed out and I want to avoid this.
Consider this code:
private fun btnGoogle(){
val acct = GoogleSignIn.getLastSignedInAccount(context)
Timber.d(acct?.email)
if (acct != null) {
val credential = GoogleAuthProvider.getCredential(acct.idToken, null)
auth(credential)
}
}
private fun btnFacebook(){
val token = AccessToken.getCurrentAccessToken()
if(token!=null){
val credential = FacebookAuthProvider.getCredential(token.token)
auth(credential)
}else{
activity?.showBackgroundToast(getString(R.string.no_facebook_auth), Toast.LENGTH_LONG)
}
}
private fun auth(credential: AuthCredential) {
FirebaseAuth.getInstance().currentUser!!.reauthenticate(credential)
.addOnFailureListener{e -> activity?.showBackgroundToast(e.localizedMessage, Toast.LENGTH_LONG)}
.addOnCompleteListener { task ->
if (task.isSuccessful) {
mListener.onReAuthentication(true)
dialog.dismiss()
} else {
activity?.showBackgroundToast(task.exception?.localizedMessage, Toast.LENGTH_LONG)
}
}
}
Then I call this function to actually update the email:
fun updateEmail(newEmail: String): Completable {
return Completable.create { emitter ->
val currentUser = FirebaseAuth.getInstance().currentUser
currentUser!!.updateEmail(newEmail).addOnCompleteListener {
if (it.isSuccessful) {
Timber.d("updated email to %s", newEmail)
emitter.onComplete()
} else {
emitter.onError(Throwable(it.exception?.localizedMessage))
}
}
}
}
Everything works fine until I finally update the email. When I do, Firebase signs the user out every time!! (the following is from Android Studio logcat)
D/FirebaseAuth: Notifying id token listeners about a sign-out event.
D/FirebaseAuth: Notifying auth state listeners about a sign-out event.
It only happens when I change the email. Since I have an auth state listener, the user gets redirected to login screen after successfully updating the email, which makes no sense to me. Why? How can I avoid this?