Skip to content

Instantly share code, notes, and snippets.

@grrigore
Created October 19, 2021 15:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save grrigore/cbb7da370e6647c6c69a45ea56f5f6ed to your computer and use it in GitHub Desktop.
Save grrigore/cbb7da370e6647c6c69a45ea56f5f6ed to your computer and use it in GitHub Desktop.
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.grrigore.base.utils.CoroutineContextProvider
import com.grrigore.base.utils.ResourceString
import kotlinx.coroutines.flow.MutableStateFlow
open class BaseViewModel(
val resourceString: ResourceString,
coroutineContextProvider: CoroutineContextProvider,
) : ViewModel() {
val ioContext = coroutineContextProvider.IO
private val loading = MutableStateFlow(false)
private val _error = MutableLiveData<String?>()
val error: LiveData<String?>
get() = _error
fun setError(message: String?) {
_error.postValue(message)
}
fun hasError(): Boolean {
return _error.value?.isBlank() ?: false
}
}
fun <T> BaseViewModel.makeRequestEvent(
resourceString: ResourceString,
coroutineContext: CoroutineContext,
onError: (EventResult<T>) -> Unit,
onExecute: suspend () -> Unit,
) = viewModelScope.launch(coroutineContext) {
try {
onExecute()
} catch (exception: Exception) {
FirebaseCrashlytics.getInstance().recordException(exception)
Log.d("REQUEST", exception.message, exception)
onError(Event(when (exception) {
is NetworkException -> {
Result.error(
resourceString.getString(R.string.no_internet_connection),
exception
)
}
is ForbiddenException -> {
Result.error(
exception.message
?: resourceString.getString(R.string.default_error),
exception
)
}
is UnauthorizedException -> {
Result.error(
exception.message
?: resourceString.getString(R.string.default_error),
exception
)
}
is FirebaseAuthInvalidCredentialsException -> {
Result.error(
resourceString.getString(R.string.default_error),
exception
)
}
is SSLException, is UnknownHostException -> {
Result.error(
resourceString.getString(R.string.check_internet_connection),
exception
)
}
else -> {
Result.error(
resourceString.getString(R.string.default_error),
exception
)
}
}))
}
}
import kotlinx.coroutines.Dispatchers
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
open class CoroutineContextProvider @Inject constructor() {
open val MAIN: CoroutineContext by lazy { Dispatchers.Main }
open val IO: CoroutineContext by lazy { Dispatchers.IO }
}
enum class CredentialType {
USERNAME, EMAIL
}
package com.grrigore.login.composable
import android.util.Log
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.grrigore.base.composable.MarginView
import com.grrigore.base.utils.Result.Status.*
import com.grrigore.login.R
import com.grrigore.login.utils.CredentialType
import com.grrigore.login.viewmodel.LoginViewModel
/**
* Created by Grigore Cristian-Andrei on 08/10/2021.
*/
@Composable
fun LoginScreen(
@StringRes identifierStringId: Int = R.string.username,
@StringRes passwordStringId: Int = R.string.password,
@StringRes submitStringId: Int = R.string.login,
credentialType: CredentialType = CredentialType.USERNAME,
scaffoldState: ScaffoldState = rememberScaffoldState()
) {
val viewModel: LoginViewModel = viewModel()
val viewState by viewModel.state.collectAsState()
viewState.loginResult?.getContentIfNotHandled()?.let { result ->
when (result.status) {
LOADING -> Log.d("LoginScreen", "LoginScreen: loading")
SUCCESS -> Log.d("LoginScreen", "LoginScreen: success")
ERROR -> LaunchedEffect(result.status) {
scaffoldState.snackbarHostState.showSnackbar(message = result.message!!)
}
}
}
Scaffold(scaffoldState = scaffoldState) {
Surface(color = MaterialTheme.colors.background) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CredentialsView(
identifier = viewState.identifier,
onIdentifierChange = viewModel::onIdentifierChange,
password = viewState.password,
onPasswordChange = viewModel::onPasswordChange,
identifierStringId = identifierStringId,
passwordStringId = passwordStringId,
credentialType = credentialType,
onActionDone = viewModel::login
)
Spacer(modifier = Modifier.height(16.dp))
SubmitButton(
submitStringId = submitStringId,
onClick = viewModel::login
)
}
}
}
}
@Composable
fun CredentialsView(
identifier: String,
onIdentifierChange: (String) -> Unit,
password: String,
onPasswordChange: (String) -> Unit,
@StringRes identifierStringId: Int,
@StringRes passwordStringId: Int,
credentialType: CredentialType,
onActionDone: () -> Unit
) {
MarginView(start = 16.dp, end = 16.dp) {
OutlinedTextField(
value = identifier,
onValueChange = onIdentifierChange,
placeholder = { Text(stringResource(identifierStringId)) },
maxLines = 1,
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = when (credentialType) {
CredentialType.EMAIL -> KeyboardType.Email
CredentialType.USERNAME -> KeyboardType.Text
},
imeAction = ImeAction.Next
),
modifier = Modifier
.fillMaxWidth()
.background(Color.LightGray),
)
}
Spacer(modifier = Modifier.height(16.dp))
MarginView(start = 16.dp, end = 16.dp) {
OutlinedTextField(
value = password,
onValueChange = onPasswordChange,
placeholder = { Text(stringResource(id = passwordStringId)) },
maxLines = 1,
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { onActionDone() }),
modifier = Modifier
.fillMaxWidth()
.background(Color.LightGray),
visualTransformation = PasswordVisualTransformation(),
)
}
}
@Composable
fun SubmitButton(
@StringRes submitStringId: Int,
onClick: () -> Unit,
) {
Button(onClick = onClick) {
Text(stringResource(id = submitStringId))
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
LoginScreen()
}
package com.grrigore.login.viewmodel
import com.grrigore.base.extensions.makeRequestEvent
import com.grrigore.base.utils.*
import com.grrigore.base.viewmodel.BaseViewModel
import com.grrigore.login.repository.ILoginRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
/**
* Created by Grigore Cristian-Andrei on 12/10/2021.
*/
@HiltViewModel
class LoginViewModel @Inject constructor(
resourceString: ResourceString,
coroutineContextProvider: CoroutineContextProvider,
private val repository: ILoginRepository
) : BaseViewModel(resourceString, coroutineContextProvider) {
private val _state = MutableStateFlow(LoginViewState())
val state: StateFlow<LoginViewState>
get() = _state
fun onIdentifierChange(newIdentifier: String) {
_state.value = _state.value.copy(identifier = newIdentifier)
}
fun onPasswordChange(newPassword: String) {
_state.value = _state.value.copy(password = newPassword)
}
fun login() = makeRequestEvent<Boolean>(resourceString, ioContext,
onError = {
_state.value = _state.value.copy(loginResult = it)
},
onExecute = {
_state.value = _state.value.copy(loginResult = Event(Result.loading()))
_state.value = _state.value.copy(
loginResult = Event(
repository.login(
_state.value.identifier,
_state.value.password
)
)
)
})
}
data class LoginViewState(
val identifier: String = "",
val password: String = "",
val loginResult: EventResult<Boolean>? = null,
// val success: Boolean = false,
// val loading: Boolean = false,
// val errorMessage: String? = null
)
import android.content.Context
import androidx.annotation.StringRes
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class ResourceString @Inject constructor(
@ApplicationContext private val context: Context,
) {
fun getString(@StringRes stringId: Int) = context.getString(stringId)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment