I am creating an app that right now is just supposed to be able to use Firebase authentication with mvvm as a standard to get some kind of seperation of concerns. So i have a database class that uses Firebase and gets an injection from Dagger hilt to be able to use Authentication from firebase. Now i am trying to check if the user is logged in. So i type the auth.getCurrentUser(). This does only give me the possibility to check if a user exist and with firebase when you check this and you have deleted the user while you are testing it does not update the value so it's still logged in for another hour when you have deleted the user. If you check around the internet about this you get the answer to use the authstatelistener. My question is though how to use this together with mvvm? is there a way to do this when i have my clases seperated by a viewmodel a repository and a database.
My classes look like this right now.
Database: //This has a comment written in it that has some use for the problem
import com.google.android.gms.tasks.Task
import com.google.firebase.auth.AuthResult
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.lang.Exception
import javax.inject.Inject
import javax.security.auth.callback.Callback
class FirDatabase #Inject constructor(var auth : FirebaseAuth) {
suspend fun register(user: User) : Task<AuthResult>{
return auth.createUserWithEmailAndPassword(user.email, user.password)
}
suspend fun checkIfUserExist() : Boolean? {
//i would like to be able to check it right here somehow
println("Currentuser " + auth.currentUser?.uid)
return auth.currentUser != null
}
}
Repository:
import androidx.lifecycle.MutableLiveData
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.Tasks.await
import com.google.firebase.auth.AuthResult
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AuthRepository #Inject constructor (private val database: FirDatabase) : IAuthRepository {
override suspend fun register(user: User) : Resource<AuthResult> {
return try{
val response = database.register(user)
val result = response.result
if(response.isSuccessful && result != null){
Resource.Success(result)
}else{
Resource.Error(response.exception?.message.toString())
}
}catch (e: Exception){
Resource.Error(e.message ?: "An Error occurred")
}
}
override suspend fun CheckIfloggedIn() : Resource<Boolean>{
return try{
val user = database.checkIfUserExist()
if(user != false){
Resource.IsLoggedIn("User is already logged in" )
}else{
Resource.IsNotLoggedIn("User is not logged in")
}
}catch(e: Exception){
Resource.Error(e.message ?: "An Error occurred")
}
}
}
ViewModel:
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
#HiltViewModel
class AuthViewModel #Inject constructor(private val repository: AuthRepository, private val dispatchers: DispatcherProvider) : ViewModel(){
sealed class AuthEvent{
class Success(val result: String): AuthEvent()
class Failure(val errorText: String): AuthEvent()
object Loading : AuthEvent()
object LoggedIn : AuthEvent()
object NotRegistered : AuthEvent()
object NotLoggedIn : AuthEvent()
object Empty : AuthEvent()
}
private val _registering = MutableStateFlow<AuthEvent>(AuthEvent.Empty)
val registering : StateFlow<AuthEvent> = _registering
private val _checkUserIsLoggedIn = MutableStateFlow<AuthEvent>(AuthEvent.Empty)
val checkUserIsLoggedIn : StateFlow<AuthEvent> = _checkUserIsLoggedIn
fun register(user: User){
viewModelScope.launch(dispatchers.io) {
_registering.value = AuthEvent.Loading
when(val authResponse = repository.register(user)){
is Resource.Error -> _registering.value = AuthEvent.Failure(authResponse.message!!)
is Resource.Success -> {
val response = authResponse.data!!.user
_registering.value = AuthEvent.Success("Success")
}
}
}
}
fun CheckIfUserIsLoggedIn()
{
viewModelScope.launch(dispatchers.io) {
when(val isUserLoggedIn = repository.CheckIfloggedIn()){
is Resource.IsLoggedIn -> _checkUserIsLoggedIn.value = AuthEvent.LoggedIn
is Resource.IsNotLoggedIn -> _checkUserIsLoggedIn.value = AuthEvent.NotLoggedIn
is Resource.Error -> _checkUserIsLoggedIn.value = AuthEvent.Failure(isUserLoggedIn.message!!)
}
}
}
}
I have followed tutorials from this dude https://www.youtube.com/watch?v=ct5etYgB5pQ
and i have already seen alot of the documentation on this page like this for example Firebase: how to check if user is logged in?
and here How does the firebase AuthStateListener work?. So with further investigations into the answer you gave me i have come up with this solution... but it is not really working? why is this?
Repository function:
override fun CheckIfloggedIn() = callbackFlow<Resource<Boolean>>{
val isUserLoggedIn = flow<Resource<Boolean>>{
database.checkIfUserExist().collect { isLoggedIn ->
if(isLoggedIn){
Resource.Success(isLoggedIn)
}else{
Resource.Error("User is not logged in")
}
}
}
}
Database function:
fun checkIfUserExist() = callbackFlow {
val authStatelistener = FirebaseAuth.AuthStateListener {auth ->
trySend(auth.currentUser == null)
}
auth.addAuthStateListener(authStatelistener)
awaitClose {
auth.removeAuthStateListener(authStatelistener)
}
}
Related
I am having some issues with Firebase auth. I'm building an app using using Kotlin but keep retrieving the error 'W/System: Ignoring header X-Firebase-Locale because its value was null.'
I had this working previously, when I had set up the application using activities. I've now moved towards MVP architecture, but this seems to have broken my firebase authentication. I have also ensured that Email/Password sign in method has been enabled in the Firebase console.
If anyone could please take a look and hopefully you can see where I am going wrong. Many thanks.
SignInView
package org.wit.hikingtrails.views.signIn
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import org.wit.hikingtrails.databinding.ActivitySignInBinding
import com.google.firebase.auth.FirebaseAuth
import org.wit.hikingtrails.activities.SignUpActivity
import org.wit.hikingtrails.views.hikeList.HikeListView
import org.wit.hikingtrails.views.signUp.SignUpView
class SignInView : AppCompatActivity() {
lateinit var presenter: SignInPresenter
private lateinit var binding: ActivitySignInBinding
private lateinit var firebaseAuth: FirebaseAuth
override fun onCreate(savedInstanceState: Bundle?) {
presenter = SignInPresenter( this)
super.onCreate(savedInstanceState)
binding = ActivitySignInBinding.inflate(layoutInflater)
setContentView(binding.root)
firebaseAuth = FirebaseAuth.getInstance()
binding.textView.setOnClickListener {
val intent = Intent(this, SignUpView::class.java)
startActivity(intent)
}
binding.button.setOnClickListener {
val email = binding.emailEt.text.toString()
val pass = binding.passET.text.toString()
if (email.isNotEmpty() && pass.isNotEmpty()) {
presenter.doLogin(email, pass)
} else {
Toast.makeText(this, "Empty Fields Are not Allowed !!", Toast.LENGTH_SHORT).show()
}
}
}
override fun onStart() {
super.onStart()
if(firebaseAuth.currentUser != null){
val intent = Intent(this, HikeListView::class.java)
startActivity(intent)
}
}
}
SignInPresenter:
package org.wit.hikingtrails.views.signIn
import android.content.ContentValues.TAG
import android.content.Intent
import android.util.Log
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.google.firebase.auth.FirebaseAuth
import org.wit.hikingtrails.views.hikeList.HikeListView
import timber.log.Timber
import timber.log.Timber.i
class SignInPresenter (val view: SignInView) {
private lateinit var loginIntentLauncher : ActivityResultLauncher<Intent>
init{
registerLoginCallback()
}
var auth: FirebaseAuth = FirebaseAuth.getInstance()
fun doLogin(email: String, pass: String) {
// view.showProgress()
auth.signInWithEmailAndPassword(email, pass).addOnCompleteListener(view) { task ->
if (task.isSuccessful) {
val launcherIntent = Intent(view, HikeListView::class.java)
loginIntentLauncher.launch(launcherIntent)
} else {
i("Login failed:")
// val launcherIntent = Intent(view, HikeListView::class.java)
// loginIntentLauncher.launch(launcherIntent)
}
// view.hideProgress()
}
}
private fun registerLoginCallback(){
loginIntentLauncher =
view.registerForActivityResult(ActivityResultContracts.StartActivityForResult())
{ }
}
}
For anyone that was having the same issue: The issue was with my API key. In kotlin, to debug, I used Timber.i( "signInWithCredential:failure ${task.exception?.message}") within the else statement of doLogin() in the presenter
How to fix IllegalStateException: Attempt to collect twice from pageEventFlow, which is an illegal operation.
Did you forget to call Flow<PagingData<*>>.cachedIn(coroutineScope)?
Code:
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import com.kharismarizqii.movieapp.data.MovieRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
#HiltViewModel
class MovieViewModel #Inject constructor(
private val repository: MovieRepository,
state: SavedStateHandle) : ViewModel(){
companion object{
private const val CURRENT_QUERY = "current_query"
private const val EMPTY_QUERY = ""
}
private val currentQuery = state.getLiveData(CURRENT_QUERY, EMPTY_QUERY)
val movies = currentQuery.switchMap { query ->
if (query.isNotEmpty()){
repository.getSearchMovies(query)
}else{
repository.getNowPlayingMovies().cachedIn(viewModelScope)
}
}
fun searchMovies(query: String){
currentQuery.value = query
}
}
Crash:
java.lang.IllegalStateException: Attempt to collect twice from pageEventFlow, which is an illegal operation. Did you forget to call Flow<PagingData<*>>.cachedIn(coroutineScope)?
You should cache the data for getSearchMovies too
private val currentQuery = state.getLiveData(CURRENT_QUERY, EMPTY_QUERY)
val movies = currentQuery.switchMap { query ->
if (query.isNotEmpty()){
repository.getSearchMovies(query).cachedIn(viewModelScope)
}else{
repository.getNowPlayingMovies().cachedIn(viewModelScope)
}
}
I have made a simple unit test which tests a coroutines function which uses firebase.
I've mocked all the dependencies and the methods being used in this function I'm testing, but it continues to hang. I'm not sure exactly where it's hanging, but I would assume on the mocking of firestore, and it has await().
Test Class:
import android.content.Context
import com.example.socialtoker.data.db.UserDao
import com.example.socialtoker.data.repository.UserDataRepository
import com.example.socialtoker.data.repository.UserDataRepositoryImpl
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.DatabaseReference
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.storage.FirebaseStorage
import io.mockk.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
#RunWith(JUnit4::class)
#ExperimentalCoroutinesApi
class UserDataRepositoryImplTest {
private val contextMock = mockk<Context>{
every { getExternalFilesDir(any())?.absolutePath } returns "src/SocialToker/"
}
private val firestoreMock = mockk<FirebaseFirestore>{
coEvery { collection("Users").document(any()).set(any()).await() } returns mockk()
}
private val firebaseAuthMock = mockk<FirebaseAuth>{
coEvery { createUserWithEmailAndPassword(any(), any()) } returns mockk()
every { currentUser?.uid } returns "UID"
}
private val firebaseStorageMock = mockk<FirebaseStorage>()
private val firebaseDatabaseMock = mockk<DatabaseReference>()
private val daoMock = mockk<UserDao>{
coEvery { addUser(any()) } returns mockk()
}
private lateinit var userDateRepository: UserDataRepository
private val emailAddress = "socialtoker#socialtoker.com"
private val password = "socialtokerpassword"
private val username = "socialtoker"
#Before
fun setup() {
userDateRepository = UserDataRepositoryImpl(
contextMock,
firestoreMock,
firebaseAuthMock,
firebaseStorageMock,
firebaseDatabaseMock,
daoMock
)
}
#Test
fun `createUser calls firebase and stores user info locally and remotely`() = runBlocking {
// WHEN
userDateRepository.createUser(emailAddress, password, username)
//THEN
coVerify { firebaseAuthMock.createUserWithEmailAndPassword(emailAddress, password) }
}
}
Test Subject:
override suspend fun createUser(email: String, password: String, username: String): AuthResult {
try {
val data = hashMapOf(
"name" to username
)
val authResult = firebaseAuth.createUserWithEmailAndPassword(email, password).await()
val uid = firebaseAuth.currentUser!!.uid
userDao.addUser(UserData(uid, username, "", ""))
firestoreRef.collection("Users")
.document(uid)
.set(data).await()
return authResult
} catch (error: Throwable) {
throw RepositoryError(
error.localizedMessage ?: "Unable to create user", error
)
}
}
Please note that await is an extension function on Task class.
Therefore Mocking extension functions might need to be taken into consideration.
I'm writing an application using the Android Architecture Components, originally based on the famous article, however that is now outdated and not accurate, so based on other documentation, articles and videos, I build something using the latest components, which turned out in a very simple architecture with very little code.
The idea is the app starts with its tables empty, and goes to read from a Firestore db to get its data, stores the data in a local SqlLite DB (using Room) and displays the updated data. Whenever the data is updated on Firestore, it should be updated in SqlLite and update the UI.
However, my UI (just a text box for now) is only updated when the application starts, and never ever after the DB is modified.
PorteroDao
package com.sarcobjects.portero.db
import androidx.room.*
import com.sarcobjects.portero.entities.Portero
import com.sarcobjects.portero.entities.PorteroWithLevelsAndUnits
#Dao
abstract class PorteroDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun insert(portero: Portero): Long
#Transaction
#Query("SELECT * FROM Portero WHERE porteroId == :porteroId")
abstract suspend fun getPortero(porteroId: Long): PorteroWithLevelsAndUnits
}
PorteroRepository
package com.sarcobjects.portero.repository
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.EventListener
import com.google.firebase.firestore.FirebaseFirestore
import com.sarcobjects.portero.db.PorteroDao
import com.sarcobjects.portero.entities.Portero
import com.sarcobjects.portero.entities.PorteroWithLevelsAndUnits
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import timber.log.Timber.d
import timber.log.Timber.w
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
#Singleton
class PorteroRepository #Inject constructor(
private val porteroDao: PorteroDao,
private val firestore: FirebaseFirestore
) {
#ExperimentalCoroutinesApi
suspend fun getPortero(porteroId: Long): PorteroWithLevelsAndUnits {
GlobalScope.launch {refreshPortero(porteroId)}
val portero = porteroDao.getPortero(porteroId)
d("Retrieved portero: $portero")
return portero
}
#ExperimentalCoroutinesApi
private suspend fun refreshPortero(porteroId: Long) {
d("Refreshing")
//retrieve from firestore
retrieveFromFirestore(porteroId)
.collect { portero ->
d("Retrieved and collected: $portero")
porteroDao.insert(portero)
}
}
#ExperimentalCoroutinesApi
private fun retrieveFromFirestore(porteroId: Long): Flow<Portero> = callbackFlow {
val callback = EventListener<DocumentSnapshot> { document, e ->
if (e != null) {
w(e, "Listen from Firestore failed.")
close(e)
}
d("Read successfully from Firestore")
if (document != null && document.exists()) {
//Convert to objects
val portero = document.toObject(Portero::class.java)
d("New Portero: ${portero.toString()}")
offer(portero!!)
} else {
d("Portero not found for porteroId: $porteroId")
}
}
val addSnapshotListener = firestore.collection("portero").document(porteroId.toString())
.addSnapshotListener(callback)
awaitClose { addSnapshotListener.remove()}
}
}
ButtonsViewModel
package com.sarcobjects.portero.ui.buttons
import androidx.hilt.Assisted
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.LiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import com.sarcobjects.portero.entities.PorteroWithLevelsAndUnits
import com.sarcobjects.portero.repository.PorteroRepository
import timber.log.Timber.d
class ButtonsViewModel #ViewModelInject
constructor(#Assisted savedStateHandle: SavedStateHandle, porteroRepository: PorteroRepository) : ViewModel() {
private val porteroId: Long = savedStateHandle["porteroId"] ?: 0
val portero: LiveData<PorteroWithLevelsAndUnits> = liveData {
val data = porteroRepository.getPortero(porteroId)
d("Creating LiveData with: $data")
emit(data)
}
}
ButtonsFragment
package com.sarcobjects.portero.ui.buttons
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import com.sarcobjects.portero.R
import com.sarcobjects.portero.entities.PorteroWithLevelsAndUnits
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.buttons_fragment.*
import timber.log.Timber.d
#AndroidEntryPoint
class ButtonsFragment : Fragment() {
companion object {
fun newInstance() = ButtonsFragment()
}
private val viewModel: ButtonsViewModel by viewModels (
)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.buttons_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.portero.observe(viewLifecycleOwner, Observer<PorteroWithLevelsAndUnits> {porteroWLAU ->
d("Observing portero: $porteroWLAU")
message.text = porteroWLAU?.portero?.name ?: "Portero not found."
})
}
}
All the dependency injection seems to be OK (no NPEs), I even checked that the ViewModel instance is the same on the Fragment and in the ViewModel itself, and the persistence via Room is correct; the new data is actually being saved into SqlLite when I update Firestore. Also, no exceptions or errors in logcat.
But the UI is not updated.
So, I managed to find a way to make this work, although in a different way. My idea was to make Room trigger a liveData reload whenever I wrote to SqlLite, but I never managed to make it work, and still I don't know why.
What I did in the end was:
Return a Flow from the repository, triggered by the updates in Firestore:
#ExperimentalCoroutinesApi
fun getPorteroFlow(porteroId: Long): Flow<Portero> = retrieveFromFirestore(porteroId)
#ExperimentalCoroutinesApi
private fun retrieveFromFirestore(porteroId: Long): Flow<Portero> = callbackFlow {
val callback = EventListener<DocumentSnapshot> { document, e ->
if (e != null) {
w(e, "Listen from Firestore failed.")
return#EventListener
}
d("Read successfully from Firestore")
if (document != null && document.exists()) {
//Convert to objects
val portero = document.toObject(Portero::class.java)
d("New Portero: ${portero.toString()}")
GlobalScope.launch {
d("Saved new portero: $portero")
porteroDao.insert(portero!!)
}
offer(portero!!)
} else {
d("Portero not found for porteroId: $porteroId")
}
}
val addSnapshotListener = firestore.collection("portero").document(porteroId.toString()) //.get()
.addSnapshotListener(callback)
awaitClose { addSnapshotListener.remove()}
}
Convert the Flow to liveData in the ViewModel:
private val porteroId: Long = savedStateHandle["porteroId"] ?: 0
#ExperimentalCoroutinesApi
val portero = porteroRepository.getPorteroFlow(porteroId)
.onStart { porteroRepository.getPortero(porteroId) }
.asLiveData()
}
(onStart is used to read data from SqlLite when the app starts, in case there's no internet and Firestore is unreachable).
This works flawlessly and is very fast, as soon as I update data in Firestore console, I can see the UI update in the device.
Android kotlin coroutine retrofit.
I want to get the value from the getPropeties to insert it in database. I Need a help for this? I need the value to be an instance of User not the unit value. My viewModel class is given below.
import android.app.Application
import android.content.Context
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.android.marsrealestate.database.AppDatabase
import com.example.android.marsrealestate.database.User
import com.example.android.marsrealestate.database.UserDao
import com.example.android.marsrealestate.network.UsersApi
import com.example.android.marsrealestate.network.UsersProperty
import kotlinx.coroutines.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class OverviewViewModel(val database: UserDao,
application: Application): ViewModel() {
private var viewModelJob = Job()
private val coroutineScope = CoroutineScope(
viewModelJob + Dispatchers.Main )
private var user = MutableLiveData<User?>()
// The internal MutableLiveData String that stores the most recent response
private val _response = MutableLiveData<String>()
// The external immutable LiveData for the response String
val response: LiveData<String>
get() = _response
init {
getUsersProperties()
}
private fun getUsersProperties(){
coroutineScope.launch {
var getPropertiesDeferred =
UsersApi.retrofitService.getProperties()
try {
var listResult = getPropertiesDeferred.await()
//database.insertUser(listResult)
_response.value =
"Success: ${listResult} Mars properties retrieved"
} catch (e: Exception) {
_response.value = "Failure: ${e.message}"
}
}
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
Thanks
You are using launch,
Launch is used to perform asynchronous fire and forget type of
operations where you are not interested in the result of operation.
Instead, you can use async,
Async is used to perform asynchronous computation where you expect a
result of the computation in the future
private fun getUsersProperties() =
coroutineScope.async {
var getPropertiesDeferred =
UsersApi.retrofitService.getProperties()
try {
var listResult = getPropertiesDeferred.await()
//database.insertUser(listResult)
_response.value =
"Success: ${listResult} Mars properties retrieved"
} catch (e: Exception) {
_response.value = "Failure: ${e.message}"
}
// =================================================
// ========= Return whatever result you want =======
// =================================================
}
can you also show what is the type signature of getProperties?