refactor: move network logic into ViewModels and externalize strings
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
98b15c748a
commit
43d1a6013b
@ -22,9 +22,14 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
optimization {
|
||||
enable = false
|
||||
}
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
@ -36,6 +41,9 @@ android {
|
||||
dependencies {
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.fragment.ktx)
|
||||
implementation(libs.material)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.gson)
|
||||
|
||||
@ -147,7 +147,7 @@ class JobFormFragment : Fragment() {
|
||||
private fun scrapeUrl() {
|
||||
val url = seekUrlInput.text?.toString()?.trim() ?: ""
|
||||
if (url.isEmpty()) {
|
||||
seekUrlInput.error = "Enter a URL"
|
||||
seekUrlInput.error = getString(R.string.enter_url)
|
||||
return
|
||||
}
|
||||
showLoading(true)
|
||||
|
||||
@ -14,27 +14,25 @@ import androidx.credentials.CustomCredential
|
||||
import androidx.credentials.GetCredentialRequest
|
||||
import androidx.credentials.GetCredentialResponse
|
||||
import androidx.credentials.exceptions.GetCredentialException
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.activity.viewModels
|
||||
import com.example.resbuilder.R
|
||||
import com.example.resbuilder.data.remote.ApiClient
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
|
||||
import com.google.android.gms.common.api.ApiException
|
||||
import com.google.android.gms.tasks.Task
|
||||
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
|
||||
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
|
||||
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.security.SecureRandom
|
||||
import java.util.Base64
|
||||
|
||||
class LoginActivity : AppCompatActivity() {
|
||||
|
||||
private val viewModel: LoginViewModel by viewModels()
|
||||
|
||||
private lateinit var googleSignInButton: MaterialButton
|
||||
private lateinit var progress: CircularProgressIndicator
|
||||
private lateinit var credentialManager: CredentialManager
|
||||
@ -45,10 +43,6 @@ class LoginActivity : AppCompatActivity() {
|
||||
private val webClientId = "416332591622-godddvcplbobkhv6uvabg94pvtukp61u.apps.googleusercontent.com"
|
||||
private val androidClientId = "416332591622-41ioels091g0am6v5m1catf0tt69ohk4.apps.googleusercontent.com"
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LoginActivity"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_login)
|
||||
@ -62,25 +56,36 @@ class LoginActivity : AppCompatActivity() {
|
||||
googleSignInLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartIntentSenderForResult()
|
||||
) { result ->
|
||||
Log.w(TAG, "Unexpected activity result: ${result.resultCode}")
|
||||
showLoading(false)
|
||||
Toast.makeText(this, "Sign-in interrupted", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(this, R.string.sign_in_interrupted, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
observeAuthState()
|
||||
|
||||
showLoading(true)
|
||||
checkExistingAuth()
|
||||
viewModel.checkExistingAuth()
|
||||
}
|
||||
|
||||
private fun checkExistingAuth() {
|
||||
private fun observeAuthState() {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val user = withContext(Dispatchers.IO) { ApiClient.getMe() }
|
||||
showLoading(false)
|
||||
if (user.authenticated) {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.state.collect { state ->
|
||||
showLoading(state.loading)
|
||||
state.error?.let {
|
||||
Toast.makeText(this@LoginActivity, it, Toast.LENGTH_LONG).show()
|
||||
viewModel.setLoading(false)
|
||||
}
|
||||
state.user?.let { user ->
|
||||
if (state.authenticated) {
|
||||
Toast.makeText(
|
||||
this@LoginActivity,
|
||||
getString(R.string.welcome_user, user.name ?: user.email),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
launchMain()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
showLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -90,14 +95,8 @@ class LoginActivity : AppCompatActivity() {
|
||||
googleSignInButton.isEnabled = !loading
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun launchGoogleSignIn() {
|
||||
showLoading(true)
|
||||
lifecycleScope.launch {
|
||||
Log.d(TAG, "Starting Credential Manager flow")
|
||||
Log.d(TAG, "Web client ID: $webClientId")
|
||||
Log.d(TAG, "Android client ID: $androidClientId")
|
||||
val googleIdOption = GetGoogleIdOption.Builder()
|
||||
.setFilterByAuthorizedAccounts(false)
|
||||
.setServerClientId(webClientId)
|
||||
@ -109,23 +108,19 @@ class LoginActivity : AppCompatActivity() {
|
||||
.addCredentialOption(googleIdOption)
|
||||
.build()
|
||||
|
||||
viewModel.viewModelScope.launch {
|
||||
try {
|
||||
Log.d(TAG, "Calling getCredential...")
|
||||
val result = credentialManager.getCredential(
|
||||
request = request,
|
||||
context = this@LoginActivity
|
||||
)
|
||||
Log.d(TAG, "Credential retrieved successfully")
|
||||
handleCredentialResponse(result)
|
||||
} catch (e: GetCredentialException) {
|
||||
Log.e(TAG, "Credential Manager failed: ${e.javaClass.simpleName}", e)
|
||||
Log.e(TAG, "Error message: ${e.message}")
|
||||
Log.e(TAG, "Error code: ${e.cause}")
|
||||
showLoading(false)
|
||||
val errorMsg = when (e) {
|
||||
is androidx.credentials.exceptions.NoCredentialException -> "No Google accounts found on device"
|
||||
is androidx.credentials.exceptions.GetCredentialCancellationException -> "Sign-in cancelled"
|
||||
else -> "Sign-in failed: ${e.message}"
|
||||
is androidx.credentials.exceptions.NoCredentialException -> getString(R.string.no_google_accounts)
|
||||
is androidx.credentials.exceptions.GetCredentialCancellationException -> getString(R.string.sign_in_cancelled)
|
||||
else -> getString(R.string.sign_in_failed, e.message)
|
||||
}
|
||||
Toast.makeText(this@LoginActivity, errorMsg, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
@ -141,56 +136,18 @@ class LoginActivity : AppCompatActivity() {
|
||||
val idToken = googleIdTokenCredential.idToken
|
||||
if (idToken.isNullOrEmpty()) {
|
||||
showLoading(false)
|
||||
Toast.makeText(this, "No ID token from Google", Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(this, R.string.no_id_token, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
exchangeToken(idToken)
|
||||
viewModel.exchangeToken(idToken)
|
||||
} catch (e: GoogleIdTokenParsingException) {
|
||||
showLoading(false)
|
||||
Toast.makeText(this, "Invalid Google ID token: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(this, getString(R.string.invalid_google_id_token, e.message), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
showLoading(false)
|
||||
Toast.makeText(this, "Unexpected credential type", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSignInError(message: String) {
|
||||
showLoading(false)
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun exchangeToken(idToken: String) {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val token = withContext(Dispatchers.IO) { ApiClient.exchangeGoogleToken(idToken) }
|
||||
ApiClient.setToken(token)
|
||||
val user = withContext(Dispatchers.IO) { ApiClient.getMe() }
|
||||
showLoading(false)
|
||||
if (user.authenticated) {
|
||||
Toast.makeText(
|
||||
this@LoginActivity,
|
||||
"Welcome ${user.name ?: user.email}",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
launchMain()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this@LoginActivity,
|
||||
"Server rejected the token",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showLoading(false)
|
||||
Log.e(TAG, "Token exchange failed", e)
|
||||
Toast.makeText(
|
||||
this@LoginActivity,
|
||||
e.message ?: "Token exchange failed",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
Toast.makeText(this, R.string.unexpected_credential_type, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -200,10 +157,6 @@ class LoginActivity : AppCompatActivity() {
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun generateNonce(): String {
|
||||
val secureRandom = SecureRandom()
|
||||
val bytes = ByteArray(32)
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
package com.example.resbuilder.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.resbuilder.data.model.User
|
||||
import com.example.resbuilder.data.remote.ApiClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
data class LoginUiState(
|
||||
val loading: Boolean = false,
|
||||
val authenticated: Boolean = false,
|
||||
val user: User? = null,
|
||||
val error: String? = null
|
||||
)
|
||||
|
||||
class LoginViewModel : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(LoginUiState())
|
||||
val state: StateFlow<LoginUiState> = _state.asStateFlow()
|
||||
|
||||
fun checkExistingAuth() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val user = withContext(Dispatchers.IO) { ApiClient.getMe() }
|
||||
_state.value = LoginUiState(loading = false, authenticated = user.authenticated, user = user)
|
||||
} catch (_: Exception) {
|
||||
_state.value = LoginUiState(loading = false, error = "check auth failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun exchangeToken(idToken: String) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(loading = true, error = null)
|
||||
try {
|
||||
val token = withContext(Dispatchers.IO) { ApiClient.exchangeGoogleToken(idToken) }
|
||||
ApiClient.setToken(token)
|
||||
val user = withContext(Dispatchers.IO) { ApiClient.getMe() }
|
||||
_state.value = LoginUiState(
|
||||
loading = false,
|
||||
authenticated = user.authenticated,
|
||||
user = user
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
_state.value = LoginUiState(loading = false, error = e.message ?: "Token exchange failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setLoading(loading: Boolean) {
|
||||
_state.value = _state.value.copy(loading = loading)
|
||||
}
|
||||
|
||||
fun consumeAuth() {
|
||||
_state.value = _state.value.copy(authenticated = false)
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,6 @@
|
||||
package com.example.resbuilder.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -11,15 +9,18 @@ import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.example.resbuilder.R
|
||||
import com.example.resbuilder.data.model.Job
|
||||
import com.example.resbuilder.data.model.JobSummary
|
||||
import com.example.resbuilder.data.remote.ApiClient
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MyResumesFragment : Fragment() {
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val viewModel: MyResumesViewModel by viewModels()
|
||||
|
||||
private lateinit var emptyView: TextView
|
||||
private lateinit var progress: LinearProgressIndicator
|
||||
@ -37,71 +38,54 @@ class MyResumesFragment : Fragment() {
|
||||
|
||||
listView.setOnItemClickListener { _, _, position, _ ->
|
||||
val item = listView.adapter.getItem(position) as JobSummary
|
||||
openJob(item.job_id)
|
||||
viewModel.openJob(item.job_id)
|
||||
}
|
||||
|
||||
loadJobs()
|
||||
observeState()
|
||||
viewModel.loadJobs()
|
||||
}
|
||||
|
||||
private fun loadJobs() {
|
||||
showLoading(true)
|
||||
Thread {
|
||||
try {
|
||||
val resp = ApiClient.getMyJobs()
|
||||
handler.post {
|
||||
showLoading(false)
|
||||
bindJobs(resp.jobs)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
handler.post {
|
||||
showLoading(false)
|
||||
private fun observeState() {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.state.collect { state ->
|
||||
progress.visibility = if (state.loading) View.VISIBLE else View.GONE
|
||||
|
||||
state.error?.let {
|
||||
emptyView.text = getString(R.string.my_resumes_error)
|
||||
emptyView.visibility = View.VISIBLE
|
||||
Toast.makeText(requireContext(), e.message ?: getString(R.string.my_resumes_error), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun bindJobs(jobs: List<JobSummary>) {
|
||||
if (jobs.isEmpty()) {
|
||||
if (!state.loading && state.jobs.isNotEmpty()) {
|
||||
emptyView.visibility = View.GONE
|
||||
listView.visibility = View.VISIBLE
|
||||
listView.adapter = JobListAdapter(requireContext(), state.jobs)
|
||||
} else if (!state.loading && state.error == null) {
|
||||
emptyView.text = getString(R.string.my_resumes_empty)
|
||||
emptyView.visibility = View.VISIBLE
|
||||
listView.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
listView.visibility = View.VISIBLE
|
||||
listView.adapter = JobListAdapter(requireContext(), jobs)
|
||||
}
|
||||
|
||||
private fun openJob(jobId: String) {
|
||||
showLoading(true)
|
||||
Thread {
|
||||
try {
|
||||
val job = ApiClient.getJob(jobId)
|
||||
handler.post {
|
||||
showLoading(false)
|
||||
if (job == null) {
|
||||
Toast.makeText(requireContext(), R.string.my_resumes_error, Toast.LENGTH_LONG).show()
|
||||
return@post
|
||||
state.openedJob?.let { job ->
|
||||
viewModel.consumeOpenedJob()
|
||||
openJobResult(job)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openJobResult(job: com.example.resbuilder.data.model.Job) {
|
||||
when (job.status.uppercase()) {
|
||||
"COMPLETED" -> {
|
||||
val type = job.type ?: JobFormFragment.TYPE_RESUME
|
||||
replaceWith(ResultFragment.newInstance(job, type))
|
||||
}
|
||||
"FAILED" -> Toast.makeText(requireContext(), job.error ?: getString(R.string.status_failed), Toast.LENGTH_LONG).show()
|
||||
else -> replaceWith(JobStatusFragment.newInstance(jobId, job.type ?: JobFormFragment.TYPE_RESUME))
|
||||
else -> replaceWith(JobStatusFragment.newInstance(job.job_id ?: "", job.type ?: JobFormFragment.TYPE_RESUME))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
handler.post {
|
||||
showLoading(false)
|
||||
Toast.makeText(requireContext(), e.message ?: getString(R.string.my_resumes_error), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun replaceWith(fragment: Fragment) {
|
||||
requireActivity().supportFragmentManager.beginTransaction()
|
||||
@ -110,15 +94,6 @@ class MyResumesFragment : Fragment() {
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
|
||||
private fun showLoading(loading: Boolean) {
|
||||
progress.visibility = if (loading) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
private class JobListAdapter(
|
||||
context: android.content.Context,
|
||||
private val jobs: List<JobSummary>
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
package com.example.resbuilder.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.resbuilder.data.model.Job
|
||||
import com.example.resbuilder.data.model.JobSummary
|
||||
import com.example.resbuilder.data.remote.ApiClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
data class MyResumesUiState(
|
||||
val loading: Boolean = false,
|
||||
val jobs: List<JobSummary> = emptyList(),
|
||||
val error: String? = null,
|
||||
val openedJob: Job? = null
|
||||
)
|
||||
|
||||
class MyResumesViewModel : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(MyResumesUiState())
|
||||
val state: StateFlow<MyResumesUiState> = _state.asStateFlow()
|
||||
|
||||
fun loadJobs() {
|
||||
_state.value = _state.value.copy(loading = true, error = null)
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val jobs = withContext(Dispatchers.IO) { ApiClient.getMyJobs().jobs }
|
||||
_state.value = _state.value.copy(loading = false, jobs = jobs)
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(loading = false, error = e.message ?: "load failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openJob(jobId: String) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(loading = true, error = null)
|
||||
try {
|
||||
val job = withContext(Dispatchers.IO) { ApiClient.getJob(jobId) }
|
||||
_state.value = _state.value.copy(loading = false, openedJob = job)
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(loading = false, error = e.message ?: "open failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeOpenedJob() {
|
||||
_state.value = _state.value.copy(openedJob = null)
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,14 @@
|
||||
<string name="checking_auth">Checking authentication…</string>
|
||||
<string name="welcome_subtitle">AI-powered resume and cover letter builder</string>
|
||||
<string name="sign_in_terms">By signing in, you agree to our Terms of Service and Privacy Policy</string>
|
||||
<string name="sign_in_interrupted">Sign-in interrupted</string>
|
||||
<string name="no_id_token">No ID token from Google</string>
|
||||
<string name="invalid_google_id_token">Invalid Google ID token: %1$s</string>
|
||||
<string name="unexpected_credential_type">Unexpected credential type</string>
|
||||
<string name="no_google_accounts">No Google accounts found on device</string>
|
||||
<string name="sign_in_cancelled">Sign-in cancelled</string>
|
||||
<string name="sign_in_failed">Sign-in failed: %1$s</string>
|
||||
<string name="welcome_user">Welcome %1$s</string>
|
||||
|
||||
<!-- Main -->
|
||||
<string name="app_title">resBuilder</string>
|
||||
@ -28,6 +36,7 @@
|
||||
<string name="generate_cover_letter">Generate Cover Letter</string>
|
||||
<string name="please_log_in">Please log in to continue</string>
|
||||
<string name="fields_required">Please fill in all fields</string>
|
||||
<string name="enter_url">Enter a URL</string>
|
||||
|
||||
<!-- Status -->
|
||||
<string name="status_queued">Queued</string>
|
||||
|
||||
@ -13,9 +13,15 @@ googlePlayServicesAuth = "20.7.0"
|
||||
credentialManager = "1.5.0-beta01"
|
||||
googleid = "1.1.1"
|
||||
kotlinxCoroutines = "1.9.0"
|
||||
lifecycleViewmodel = "2.8.7"
|
||||
lifecycleRuntime = "2.8.7"
|
||||
fragmentKtx = "1.8.5"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodel" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntime" }
|
||||
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user