I would like to navigate from my login screen to home screen in case the user is logged in. Being new to kotlin and jetpack compose, I'm not sure how this can be done since SetContent{}
should only be called from on create method and the Navigation() component can only be called from within a composable. Back in Java+XMl format coding we used to use intents and StartActivity(), but since jetpack compose recommends a single activity and multiple screen programming style, I don't understand this
class MainActivity : ComponentActivity() {
//private lateinit var auth: FirebaseAuth
lateinit var mGoogleSignInClient: GoogleSignInClient
private lateinit var signInLauncher: ActivityResultLauncher<Intent>
override fun onStart() {
super.onStart()
// Check if user is signed in (non-null) and update UI accordingly.
val account = GoogleSignIn.getLastSignedInAccount(this);
if (account != null) {
// in XML style , we used to use
// Intent intent = new Intent(LoginActivity.this, UserHomeActivity.class);
// startActivity(intent);
account.getEmail()?.let { Log.d("Signed In: ", it) };
} else {
Log.d("Signed In: ", "Not signed in");
}
}
override fun onCreate(savedInstanceState: Bundle?) {
val gso =
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).requestEmail().build()
mGoogleSignInClient = GoogleSignIn.getClient(
this,
gso
)
signInLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
ActivityResultCallback<ActivityResult> { result ->
val task: Task<GoogleSignInAccount> =
GoogleSignIn.getSignedInAccountFromIntent(result.getData())
handleSignInResult(task)
})
super.onCreate(savedInstanceState)
setContent{
Navigation(mGoogleSignInClient = mGoogleSignInClient, signInLauncher = signInLauncher)
}
private fun handleSignInResult(completedTask: Task<GoogleSignInAccount>
// ,navController: NavController
) {
try {
val account = completedTask.getResult(ApiException::class.java)
val idToken = account.idToken
// navController.navigate(Screens.UserHomeScreen.route)
// val intent = Intent(this#LoginActivity, UserHomeActivity::class.java)
// startActivity(intent)
} catch (e: ApiException) {
Log.w("Sign In Error", "signInResult:failed code=" + e.statusCode)
Toast.makeText(this, "Sign in failed", Toast.LENGTH_SHORT).show()
}
}
Call your NavHost() function inside the setContent {} block in your main activity. In your NavHost() function your startDestination should be your login screen. And move your login logic to the LoginScreen composable. Something similar to below:
override fun onCreate(savedInstanceState: Bundle?) {
NavigationHost(navController = rememberNavController())
}
and
#Composable
fun NavigationHost(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = Screen.LoginScreen.route
) {
...
}
}
Related
In this app, I have a logout button that's signed out the user if logged in with email & password or with a google account, the problem happens after signing out and re-launch the app again in the SplashActivity, I do check for the currentUser if is null it should go to the login page, else it should go to the home page, but It seems the app remember the current user even it's logged out before, I tried this solution but it doesn't work
Gif describes the problem
the dagger hilt module
#Module
#InstallIn(SingletonComponent::class)
object AppModule {
#Provides
#Singleton
fun provideFirebaseAuth() = FirebaseAuth.getInstance()
#Provides
#Singleton
fun provideFirebaseFirestore() = FirebaseFirestore.getInstance()
}
SplashActivity code
private const val TAG = "SplashActivity"
#AndroidEntryPoint
class SplashActivity : AppCompatActivity() {
private lateinit var binding: ActivitySplashBinding
#Inject
lateinit var firebaseAuth: FirebaseAuth
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySplashBinding.inflate(layoutInflater)
setContentView(binding.root)
val typeface = ResourcesCompat.getFont(this, R.font.blacklist)
val animation = AnimationUtils.loadAnimation(this, R.anim.my_anim)
binding.apply {
splashTextView.typeface = typeface
splashTextView.animation = animation
}
Log.d(TAG, "onCreate: ${firebaseAuth.currentUser?.providerData?.get(0)?.email}")
}
override fun onStart() {
super.onStart()
Handler(Looper.getMainLooper()).postDelayed({ /* Create an Intent that will start the Menu-Activity. */
if(firebaseAuth.currentUser == null) {
val intent = Intent(this#SplashActivity, MainActivity::class.java)
startActivity(intent)
this#SplashActivity.startActivity(intent)
this#SplashActivity.finish()
}else{
val intent = Intent(this#SplashActivity, HomeActivity::class.java)
startActivity(intent)
this#SplashActivity.startActivity(intent)
this#SplashActivity.finish()
}
}, 3000)
}
}
ProfileFragment "logout button code"
private const val TAG = "ProfileFragment"
#AndroidEntryPoint
class ProfileFragment : Fragment() {
private var _binding: FragmentProfileBinding? = null
private val binding get() = _binding!!
#Inject
lateinit var firebaseAuth: FirebaseAuth
private lateinit var googleSignInClient: GoogleSignInClient
private val viewModel by viewModels<CredentialsViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentProfileBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.logoutButton.setOnClickListener {
val currentUser = firebaseAuth.currentUser ?: return#setOnClickListener
for (i in 0 until currentUser.providerData.size) {
when (currentUser.providerData[i].providerId) {
"google.com" -> {
//User signed in with a custom account
Log.d(
TAG,
"onViewCreated: provider is ${currentUser.providerData[i].providerId}"
)
googleSignInClient = GoogleSignIn.getClient(
requireActivity(),
viewModel.getGoogleSignInOptions()
)
googleSignInClient.signOut().addOnCompleteListener {
if (it.isSuccessful) {
redirectToMainActivity()
}else {
return#addOnCompleteListener
}
}
}
"password" -> {
Log.d(TAG, "onViewCreated: ${currentUser.providerData[i].providerId}")
firebaseAuth.signOut()
redirectToMainActivity()
}
else -> {
Log.d(TAG, "onViewCreated: ${currentUser.providerData[i].providerId}")
redirectToMainActivity()
return#setOnClickListener
}
}
}
}
}
private fun redirectToMainActivity() {
val intent = Intent(requireContext(), MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
requireActivity().finish()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
When a user is authenticated with email and password, in order to sign the user out, you only need to call:
firebaseAuth.signOut()
On the other hand, when you need to sign the user out from Google using:
googleSignInClient.signOut().addOnCompleteListener { /* ... /* }
You're only singing out the user from Google. This operation doesn't sign out the user from Firebase automatically. So to sign the user out completely, you have to use:
firebaseAuth.signOut() //Sign out from Firebase
googleSignInClient.signOut().addOnCompleteListener { /* ... /* } //Sign out from Google
In my app I want to send info to server and after receiving successful response I want to pass info to current screen to navigate to another screen.
Here's the flow:
From UI I call viewModel to send request to server. In ViewModel I have a callback:
#HiltViewModel
class CreateAccountViewModel #Inject constructor(
private val cs: CS
) : ViewModel() {
private val _screen = mutableStateOf("")
val screen: State<String> = _screen
fun setScreen(screen: Screen) {
_screen.value = screen.route
}
private val signUpCallback = object : SignUpHandler {
override fun onSuccess(user: User?, signUpResult: SignUpResult?) {
setScreen(Screen.VerifyAccountScreen)
Log.i(Constants.TAG, "sign up success")
}
override fun onFailure(exception: Exception?) {
Log.i(Constants.TAG, "sign up failure ")
}
}
}
As you can see I have also State responsible for Screen so when response is successful I want to update the state so UI layer (Screen) knows that it should navigate to another screen. My question is: how can I observer State in
#Composable
fun CreateAccountScreen(
navController: NavController,
viewModel: CreateAccountViewModel = hiltViewModel()
) {
}
Or is there a better way to achieve that?
I think your view model should know nothing about navigation routes. Simple verificationNeeded flag will be enough in this case:
var verificationNeeded by mutableStateOf(false)
private set
private val signUpCallback = object : SignUpHandler {
override fun onSuccess(user: User?, signUpResult: SignUpResult?) {
verificationNeeded = true
Log.i(Constants.TAG, "sign up success")
}
override fun onFailure(exception: Exception?) {
Log.i(Constants.TAG, "sign up failure ")
}
}
The best practice is not sharing navController outside of the view managing the NavHost, and only pass even handlers. It may be useful when you need to test or preview your screen.
Here's how you can navigate when this flag is changed:
#Composable
fun CreateAccountScreen(
onRequestVerification: () -> Unit,
viewModel: CreateAccountViewModel = hiltViewModel(),
) {
if (viewModel.verificationNeeded) {
LaunchedEffect(Unit) {
onRequestVerification()
}
}
}
in your navigation managing view:
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.CreateAccount
) {
composable(Screen.CreateAccount) {
CreateAccountScreen(
onRequestVerification = {
navController.navigate(Screen.VerifyAccountScreen)
}
)
}
}
I wanted to integrate google sign In in my app using firebase and I followed all the instructions from enabling google sign In in my firebase console and adding SHA-1 certificate fingerprint and adding the google-services.json file in the app directory.
But whenever I am trying to sign In after selecting the google account the response code is coming out be 0 always and hence unable to sign In.
Here is the Code:-
class SignInActivity : AppCompatActivity() {
private lateinit var googleSignInClient: GoogleSignInClient
private lateinit var auth:FirebaseAuth
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sign_in)
val gso= GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build()
googleSignInClient = GoogleSignIn.getClient(this,gso)
signInButton.setOnClickListener {
resultLauncher.launch(googleSignInClient.signInIntent)
}
}
private var resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){result->
Log.i("resultCode",result.resultCode.toString())
val intent=result.data
Log.i("intentData",intent.toString())
if(result.resultCode == Activity.RESULT_OK)
{
Log.i("resultCode","reachedHere")
val task = GoogleSignIn.getSignedInAccountFromIntent(intent)
handleSignInResult(task)
}
else{
Log.i("unsuccessfulSignIN",result.resultCode.toString())
}
}
private fun handleSignInResult(task: Task<GoogleSignInAccount>?) {
try{
val account= task?.getResult(ApiException::class.java)!!
Log.i("account","firebaseAuthWithGoogle:"+account.id)
firebaseAuthWithGoogle(account.idToken!!)
}catch (e : ApiException){
Log.i(ContentValues.TAG, "Google sign in failed", e)
}
}
private fun firebaseAuthWithGoogle(idToken: String) {
signInButton.visibility= View.GONE
progressBar.visibility= View.VISIBLE
val credential= GoogleAuthProvider.getCredential(idToken,null)
GlobalScope.launch(Dispatchers.IO) {
val auth=auth.signInWithCredential(credential).await()
val firebaseUser=auth.user
Log.i("user",firebaseUser.toString())
withContext(Dispatchers.Main){
updateUI(firebaseUser)
}
}
}
private fun updateUI(firebaseUser: FirebaseUser?) {
if(firebaseUser != null)
{
val intent= Intent(this,MainActivity::class.java)
startActivity(intent)
Log.i("intent","Intent Started")
// Toast.makeText(applicationContext,"Sign In Successful",Toast.LENGTH_SHORT).show()
finish()
}
else{
signInButton.visibility= View.VISIBLE
progressBar.visibility=View.GONE
Toast.makeText(this,"Sign In failed", Toast.LENGTH_SHORT).show()
}
}
}
Here is the logcat ( I have highlighted the result code that I logged)
I am very new to android development and this my first time working with firebase
I wanted to add the sign in with google feature of firebase I connected my app to firebase then added the SHA-1 certificate fingerprint and wrote the below code.
Once I click on the sign in button the intent opens a chooser to select a google account but after selecting one account nothing happens even there is no exceptions or error in the logcat.
Code:
class SignInActivity : AppCompatActivity() {
private lateinit var googleSignInClient:GoogleSignInClient
private lateinit var auth:FirebaseAuth
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sign_in)
val gso= GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build()
googleSignInClient = GoogleSignIn.getClient(this,gso)
signInButton.setOnClickListener {
resultLauncher.launch(googleSignInClient.signInIntent)
}
}
private var resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){
val intent=it.data
if(it.resultCode == Activity.RESULT_OK)
{
val task = GoogleSignIn.getSignedInAccountFromIntent(intent)
handleSignInResult(task)
}
}
private fun handleSignInResult(task: Task<GoogleSignInAccount>?) {
try{
val account= task?.getResult(ApiException::class.java)!!
Log.i("account","firebaseAuthWithGoogle:"+account.id)
firebaseAuthWithGoogle(account.idToken!!)
}catch (e : ApiException){
Log.i(TAG, "Google sign in failed", e)
}
}
private fun firebaseAuthWithGoogle(idToken: String) {
signInButton.visibility= View.GONE
progressBar.visibility=View.VISIBLE
val credential=GoogleAuthProvider.getCredential(idToken,null)
GlobalScope.launch(Dispatchers.IO) {
val auth=auth.signInWithCredential(credential).await()
val firebaseUser=auth.user
Log.i("user",firebaseUser.toString())
withContext(Dispatchers.Main){
updateUI(firebaseUser)
}
}
}
private fun updateUI(firebaseUser: FirebaseUser?) {
if(firebaseUser != null)
{
val intent=Intent(this,MainActivity::class.java)
startActivity(intent)
Log.i("intent","Intent Started")
// Toast.makeText(applicationContext,"Sign In Successful",Toast.LENGTH_SHORT).show()
finish()
}
else{
signInButton.visibility= View.VISIBLE
progressBar.visibility=View.GONE
Toast.makeText(this,"Sign In failed",Toast.LENGTH_SHORT).show()
}
}
}
Logcat:
I am very new to android development and this is my first time with firebase.
So I have followed the advice of 'yoursTruly' and created an AuthStateListener in my Activity (as described here: https://www.youtube.com/watch?v=6CXUNcsQPgQ&feature=youtu.be).
This works really well, in the fact that the Activity is simply listening to Authentication and will divert to next Activity if someone logs in and the Fragment simply calls signIn(email, password) via the ViewModel.
However, can not see a way to look for failed login attempts (so the UI will just look like it is unresponsive).
My structure is as follows: Activity -> Fragment -> ViewModel -> Repository.
I'm using DataBinding & Navigation.
Activity
class LoginActivity : AppCompatActivity(), FirebaseAuth.AuthStateListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.login_activity)
}
override fun onStart() {
super.onStart()
FirebaseAuth.getInstance().addAuthStateListener(this)
}
override fun onStop() {
super.onStop()
FirebaseAuth.getInstance().removeAuthStateListener(this)
}
override fun onAuthStateChanged(firebaseAuth: FirebaseAuth) {
// Will only fire if state has changed!
if (FirebaseAuth.getInstance().currentUser == null) {
Toast.makeText(this,"Welcome to the Locators App!\n\nPlease login to continue", Toast.LENGTH_LONG).show()
return
}
firebaseAuth.currentUser?.getIdToken(true)
?.addOnSuccessListener { result ->
val idToken = result.token
Toast.makeText(this,"User Signed In", Toast.LENGTH_LONG).show()
Log.d(TAG, "GetTokenResult result (check this at https://jwt.io/ = $idToken")
goToSiteActivity()
}
}
private fun goToSiteActivity() {
val intent = Intent(this, SiteActivity::class.java)
startActivity(intent)
finish()
}
}
Fragment
class LoginFragment : Fragment() {
private lateinit var loginViewModel: LoginViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
val binding: LoginFragmentBinding = DataBindingUtil.inflate(
inflater, R.layout.login_fragment, container, false)
binding.apply {
loginPasswordResetText.setOnClickListener{
findNavController().navigate(R.id.action_loginFragment_to_loginPasswordResetFragment)
}
loginButton.setOnClickListener{
loginProgressBar.visibility = View.VISIBLE
val email = loginEmailEditText.text.toString()
val password = loginPasswordEditText.text.toString()
if(validateForm(email, password)) {
loginViewModel.loginUser(email, password)
}
loginProgressBar.visibility = View.GONE
}
loginNewUserText.setOnClickListener{
findNavController().navigate(R.id.action_loginFragment_to_loginUserRegisterFragment)
}
}
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
loginViewModel = ViewModelProviders.of(requireActivity()).get(LoginViewModel::class.java)
}
private fun validateForm(email: String, password: String): Boolean {
var validForm = true
if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
loginEmailEditText.error = "Please enter valid email address!"
validForm = false
} else loginEmailEditText.error = null
if (password.isEmpty()) {
loginPasswordEditText.error = "Please enter password!"
validForm = false
} else loginPasswordEditText.error = null
Log.d(TAG,"validateForm: (email = $email, password = $password, validateForm = $validForm)")
return validForm
}
}
ViewModel
class LoginViewModel : ViewModel() {
fun loginUser (email: String, password: String) {
firestoreRepository.loginUser(email, password)
}
}
Repository
class FirestoreRepository {
var firebaseAuth = FirebaseAuth.getInstance()
var firebaseUser = firebaseAuth.currentUser
var failedLogin: Boolean = false
fun loginUser(email: String, password: String) {
failedLogin = false
firebaseAuth.signInWithEmailAndPassword(email, password).addOnCompleteListener {
if (it.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "signInWithEmail:success")
} else {
// If sign in fails, display a message to the user.
Log.d(TAG, "signInWithEmail:failure", it.exception)
failedLogin = true
}
}
}
}
This question ties in with a larger query of, if you create LiveData in Repo, how do you Observe from ViewModel (i.e. what do you use as the LifeCycleOwner?), but I will ask on a seperate question..
An AuthStateListener is not sufficient to determine when a sign-in fails. It will only tell you when the state of the user changes between signed in and signed out.
You will have to use the result of firebaseAuth.signInWithEmailAndPassword to determine if the sign-in failed. It looks like you already have some code there to handle that case, but you're not doing much with the error other than setting a property.
What you should do instead is have your loginUser ViewModel method return a LiveData that gets notified when the sign-in succeeds or fails. You will have to wire up the call to signInWithEmailAndPassword to change the state of that LiveData, and your view will have to observe that LiveData to show a message to the user if necessary.