We will cover briefly:
- Integrating the Google SignIn
- MVVM architecture
- Using Moshi to send data
- (Optional) Check for the previously signed-in user
Note: This article assumes the reader knows about Jetpack Compose
Integrating the Google SignIn
Prerequisite: You need to have a project inside the GoogleCloudPlatform.
Before we start integrating, we copy the client id (as received from the Google Cloud Platform) and paste it inside our res->strings.xml
<string name="google_cloud_server_client_id">YOUR_ID</string>
Setup
- Install the dependencies inside
build.gradle
of your app
implementation 'com.google.android.gms:play-services-auth:19.2.0'
- We can now access the methods for signing in with Google. Let’s create a class (GoogleApiContract) that is responsible for calling the google APIs
- This class extends from ActivityResultContract then we override the method createIntent
To configure Google Sign-In to request users' ID and basic profile information, create a GoogleSignInOptions
object with the DEFAULT_SIGN_IN
parameter. To request users' email addresses as well, create the GoogleSignInOptions
object with the requestEmail
option.
override fun createIntent(context: Context, input: Int?): Intent {
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken("YOUR_ID_HERE")
.requestEmail()
.build();
val intent = GoogleSignIn.getClient(context,gso)
return intent.signInIntent
}
- Lastly, we override the method parseResult
After the user signs in, you can get a GoogleSignInAccount
the object for the user in the activity's parseResult
method. Otherwise, we return null
override fun parseResult(resultCode: Int, intent: Intent?): Task<GoogleSignInAccount>? {
return when (resultCode) {
Activity.RESULT_OK -> {
GoogleSignIn.getSignedInAccountFromIntent(intent)
}
else -> null
}
}
MVVM Architecture
We follow the MVVM (Model-View-ViewModel) architecture in implementing the next steps
Model (M)
We create a model aka data class GoogleUserModel
data class GoogleUserModel(
val name: String?,
val email: String?
)
ViewModel (VM)
We create a ViewModel class SignInGoogleViewModel which extends AndroidViewModel
class SignInGoogleViewModel(application: Application) : AndroidViewModel(application) {
private var _userState = MutableLiveData<GoogleUserModel>()
val googleUser: LiveData<GoogleUserModel> = _userState
private var _loadingState = MutableLiveData(false)
val loading: LiveData<Boolean> = _loadingState
fun fetchSignInUser(email: String?, name: String?) {
_loadingState.value = true
viewModelScope.launch {
_userState.value =
GoogleUserModel(
email = email,
name = name,
)
}
_loadingState.value = false
}
fun hideLoading() {
_loadingState.value = false
}
fun showLoading() {
_loadingState.value = true
}
}
- We have one
LiveData
of typeGoogleUserModel
which listens to changes if it happens on the model class - The other
LiveData
is listening to the loading, if the loading state is to be shown or not - We have a method
fetchSignInUser
which is responsible for changing the state of our model class. We mark the loading state as true, when the change is about to start, and set it to false when it's done.
View (V)
We create an authentication screen that comprises our SignInGoogle Compose button.
- Our Auth Screen instantiates the SignInGoogleViewModel and listens to the changes on the GoogleUserModel
val mSignInViewModel: SignInGoogleViewModel =
viewModel(factory = SignInGoogleViewModelFactory(context.applicationContext as Application))
val state = mSignInViewModel.googleUser.observeAsState()
val user = state.value
Note: We are using the factory of our ViewModel to create an instance.
Since our googleUser is of the type LiveData
we observe the changes to it and get notified inside here.
- We create a separate compose component (AuthView) and expose the parameters as
@Composable
fun AuthView(
onClick: () -> Unit,
isError: Boolean = false,
mSignInViewModel: SignInGoogleViewModel
)
This takes in the click action, which gets called on the SignInGoogle component
SignInGoogleButton(
onClick = {
mSignInViewModel.showLoading()
onClick()
},
)
This is how the preview of this compose component looks.
- Next, we define the click function to be called from Auth Screen. This function should initiate the Google API calls.
val authResultLauncher =
rememberLauncherForActivityResult(contract = GoogleApiContract()) { task ->
try {
val gsa = task?.getResult(ApiException::class.java)
if (gsa != null) {
mSignInViewModel.fetchSignInUser(gsa.email, gsa.displayName)
} else {
isError.value = true
}
} catch (e: ApiException) {
Timber.d("Error in AuthScreen%s", e.toString())
}
}
We make use of rememberLauncherForActivityResult
for launching the GoogleSignIn activity for the result, as specified inside ourGoogleApiContract
- Once we get back the response from our contract, we extract the result using
getResult
and if the response is not null, we call the view model'sfetchSignInUser
(This method is responsible for changing the state) - In case of error, we set the error parameter to true and render the UI accordingly
- Lastly, we pass in the parameters to our AuthView (refer above)
AuthView(
onClick = { authResultLauncher.launch(signInRequestCode) },
isError = isError.value,
mSignInViewModel
)
Note:
authResultLauncher.launch
is the function responsible for initiating the Google SignIn API Call
Using Moshi to send data
After the user signs in, we need to show the user’s details on the home screen.
To achieve this, we need to pass the data from the authentication screen to the home screen.
Introducing Moshi
Moshi is a modern JSON library for Android and Java. It makes it easy to parse JSON into Java objects
Setup
- Install the dependencies inside
build.gradle
of your app
implementation "com.squareup.moshi:moshi:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
Implement Sending and Receiving data
- We change our route Home to include a user parameter
home/{user}
- Next, we annotate our data class with
generateAdapter
and change it to
@JsonClass(generateAdapter = true)
data class GoogleUserModel(
@Json(name = "name")
val name: String?,
@Json(name = "email")
val email: String?
)
- We prepare the data to be sent from Auth Screen. Once we receive the GoogleUserModel, we convert it to JSON using Moshi. Finally, we replace the
{user}
with the JSON
val moshi = Moshi.Builder().build()
val jsonAdapter=moshi.adapter(GoogleUserModel::class.java).lenient()
val userJson = jsonAdapter.toJson(user)
navController.navigate(Destinations.Home.replace("{user}",userJson))
- Finally, for receiving the data on the Home Screen, we extract the arguments received by the navigation controller.
composable(Destinations.Home) { backStackEntry ->
val userJson = backStackEntry.arguments?.getString("user")
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(GoogleUserModel::class.java)
val userObject = jsonAdapter.fromJson(userJson!!)
HomeView(navController, userModel = userObject!!)
}
The argument is received inside the back stack entry, converted to the GoogleUserModel, and passed as the parameter to the view.
Check for the previously signed-in user
Once the user signs in for the first time, we should not show them the sign-in screen again (after they come back to our app)
- We create a new function
checkSignedInUser
and call it inside theinit
method of our ViewModel
private fun checkSignedInUser(applicationContext: Context) {
_loadingState.value = true
val gsa = GoogleSignIn.getLastSignedInAccount(applicationContext)
if (gsa != null) {
_userState.value =
GoogleUserModel(
email = gsa.email,
name = gsa.displayName,
)
}
_loadingState.value = false
}
- In this function, we call the
GoogleSignIn.getLastSignedInAccount
and set the loading to true
If this returns GoogleSignInAccount
object (rather than null
), the user has already signed in to your app with Google.
- We finally set the loading to false and navigate to the HomeView (from our ViewModel)
Top comments (1)
Good article!
Tip: code is hard to learn without syntax coloring. Start your code block with the language name, and they will highlight it for you, for example: