I'm pretty new with Kotlin (and also I've never coded in Java), and I want to know if there is another way to to this, or if its okay.
To put you in context, I'm working with the Model-View-Presenter. The presenter here, receives the username and login from the login screen, and in order to handle errors (such as empty fields, invalid email), I created an Enum (with possible errors), and then I fill the list of these errors by checking conditions.
Presenter:
fun onLoginButtonClicked(email: String, password: String) {
val errorEnum = mutableListOf<ErrorEnum>()
if (email.isEmpty()) errorEnum.add(ErrorEnum.EMPTY_EMAIL)
if (password.isEmpty()) errorEnum.add(ErrorEnum.EMPTY_PASSWORD)
if (email.isNotEmpty() && !Patterns.EMAIL_ADDRESS.matcher(email).matches()) errorEnum.add(ErrorEnum.INVALID_EMAIL)
if (errorEnum.isEmpty()) {
userSession.email = email
userSession.password = password
view?.goToViewPager(email)
} else {
view?.checkErrors(errorEnum)
}
}
After that, the fragment reflects on the view the errors, iterating through the list.
Fragment:
override fun checkErrors(Errors: MutableList<ErrorEnum>) {
Errors.forEach {
when (it) {
ErrorEnum.EMPTY_PASSWORD -> binding.password.error = getString(R.string.login_alert_input)
ErrorEnum.INVALID_EMAIL -> binding.email.error = getString(R.string.login_alert_bad_email)
ErrorEnum.EMPTY_EMAIL -> binding.email.error = getString(R.string.login_alert_input)
}
}
}
Clearly, you are doing redundant work here, first your are building up List and then passing the List to View and again View is doing some work to iterate and compare, instead you can have separate methods in view to handle each without comparison and call them directly
override fun showEmailInvalidError() {
binding.password.error = getString(R.string.login_alert_input)
}
override fun showEmptyEmailError() {
binding.email.error = getString(R.string.login_alert_input)
}
override fun showEmptyPasswordError() {
binding.email.error = getString(R.string.login_alert_bad_email)
}
In presenter
fun onLoginButtonClicked(email: String, password: String) {
if(email.isEmpty()) view?.showEmptyEmailError()
if(password.isEmpty()) view?.showEmptyPasswordError()
if(email.isNotEmpty() && !Patterns.EMAIL_ADDRESS.matcher(email).matches())
view?.showEmailInvalidError()
if(errorEnum.isEmpty()) {
userSession.email = email
userSession.password = password
view?.goToViewPager(email)
}else {
view?.checkErrors(errorEnum)
}
}
Related
I have a situation where I want the livedata to be observed only once in the app. The problem is that I am working on the authentication for an app using some Node.js backend.
As I am sending the values to receive the response from the backend it's working fine till now. I observe that response and based on that I make changes to my fragment ( that is if the response received is true then move to next fragment, otherwise if it is false show a toast message ).
Now the problem is that :
Case 1: I opened the app, entered the right credentials and pressed the button, received true response from the server and goes to the next fragment.
Case 2: I opened the app, but entered the wrong credentials, I received a false from server and based on that the Toast is shown.
Case 3 (The issue): I opened the app, entered the wrong credentials and then without closing the fragment screen entered the right credentials by editing them, the app crashes and at the same time I receive multiple responses from the server via LiveData.
My observation: Looking more into that I found that the LiveData is attached to the fragment/activity and therefore it shows the last state. So as in case 3 the the last state was receiving the false value from backend it was used again and we were shown the error instead of going to the next screen.
Can anyone guide me how to solve this. Thanks
Some code that might be needed:
binding.btnContinue.setOnClickListener {
val number = binding.etMobileNumber.text.toString().toLong()
Timber.d("Number: $number")
authRiderViewModel.authDriver(number)
checkNumber()
}
Function which checks the number :
private fun checkNumber() {
authRiderViewModel.response.observe(viewLifecycleOwner, Observer {
Timber.d("Response: $it")
if (it!!.success == true) {
val action = LoginFragmentDirections.actionLoginFragmentToOtpFragment()
findNavController().navigate(action)
Timber.d("${it.message}")
} else {
Toast.makeText(requireContext(), "Number not registered", Toast.LENGTH_SHORT).show()
binding.etMobileNumber.setText("")
}
})
}
ViewModel code:
private val _response = MutableLiveData<AuthResponse>()
val response: LiveData<AuthResponse>
get() = _response
fun authDriver(number: Long) = viewModelScope.launch {
Timber.d("Number: $number")
myRepo.authDriver(number).let {
_response.postValue(it)
}
}
P.S I have tried using something called SingleLiveEvent but it doesn't seem to work.
I would create a separate class that tracks the UI state you need and update it when the state is consumed. Something like the following. I don't really know what the parameter is for authDriver, so this is a more generic example.
sealed interface AuthState {
object NotYetRequested: AuthState
object AwaitingResponse: AuthState
class ResponseReceived(val response: AuthResponse): AuthState {
var isHandled = false
private set
fun markHandled() {
isHandled = true
}
}
}
// In ViewModel:
private val _authState = MutableLiveData<AuthState>().also {
it.value = AuthState.NotYetRequested
}
val authState: LiveData<AuthState> get() = _authState
fun requestAuthentication() = viewModelScope.launch {
_authState.value = AuthState.AwaitingResponse
val response = myRepo.authenticate()
_authState.value = AuthState.ResponseReceived(response)
}
// In Fragment:
viewModel.authState.observe(viewLifecycleOwner) { authState ->
when (authState) {
AuthState.NotYetRequested -> ShowUiRequestingAuthentication()
AuthStateAwaitingResponse -> ShowIndeterminateProgressUi()
is AuthStateResponseReceived -> when {
authState.isHandled -> {} // do nothing? depends on your setup, might need to navigate to next screen if handled response is successful
authState.response.isSuccessful -> {
goToNextScreen()
authState.markHandled()
}
else -> {
showErrorToast()
ShowUiRequestingAuthentication()
authState.markHandled()
}
}
}
}
A have a screen where I display 10 users. Each user is represented by a document in Firestore. On user click, I need to get its details. This is what I have tried:
fun getUserDetails(uid: String) {
LaunchedEffect(uid) {
viewModel.getUser(uid)
}
when(val userResult = viewModel.userResult) {
is Result.Loading -> CircularProgressIndicator()
is Result.Success -> Log.d("TAG", "You requested ${userResult.data.name}")
is Result.Failure -> Log.d("TAG", userResult.e.message)
}
}
Inside the ViewModel class, I have this code:
var userResult by mutableStateOf<Result<User>>(Result.Loading)
private set
fun getUser(uid: String) = viewModelScope.launch {
repo.getUser(uid).collect { result ->
userResult = result
}
}
As you see, I use Result.Loading as a default value, because the document is heavy, and it takes time to download it. So I decided to display a progress bar. Inside the repo class I do:
override fun getUser(uid: String) = flow {
try {
emit(Result.Loading)
val user = usersRef.document(uid).get().await().toObject(User::class.java)
emit(Result.Success(user))
} catch (e: Exception) {
emit(Result.Failure(e))
}
}
I have two questions, if I may.
Is there something wrong with this code? As it works fine when I compile.
I saw some questions here, that recommend using collectAsState() or .collectAsStateWithLifecycle(). I tried changing userResult.collectAsState() but I cannot find that function. Is there any benefit in using collectAsState() or .collectAsStateWithLifecycle() than in my actual code? I'm really confused.
If you wish to follow Uncle Bob's clean architecture you can split your architecture into Data, Domain and Presentation layers.
For android image below shows how that onion shape can be simplified to
You emit your result from Repository and handle states or change data, if you Domain Driven Model, you store DTOs for data from REST api, if you have db you keep database classes instead of passing classes annotated with REST api annotation or db annotation to UI you pass a UI.
In repository you can pass data as
override fun getUser(uid: String) = flow {
val user usersRef.document(uid).get().await().toObject(User::class.java)
emit(user)
}
In UseCase you check if this returns error, or your User and then convert this to a Result or a class that returns error or success here. You can also change User data do Address for instance if your business logic requires you to return an address.
If you apply business logic inside UseCase you can unit test what you should return if you retrieve data successfully or in case error or any data manipulation happens without error without using anything related to Android. You can just take this java/kotlin class and unit test anywhere not only in Android studio.
In ViewModel after getting a Flow< Result<User>> you can pass this to Composable UI.
Since Compose requires a State to trigger recomposition you can convert your Flow with collectAsState to State and trigger recomposition with required data.
CollectAsState is nothing other than Composable function produceState
#Composable
fun <T : R, R> Flow<T>.collectAsState(
initial: R,
context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
if (context == EmptyCoroutineContext) {
collect { value = it }
} else withContext(context) {
collect { value = it }
}
}
And produceState
#Composable
fun <T> produceState(
initialValue: T,
key1: Any?,
key2: Any?,
#BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(key1, key2) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
As per discussion in comments, you can try this approach:
// Repository
suspend fun getUser(uid: String): Result<User> {
return try {
val user = usersRef.document(uid).get().await().toObject(User::class.java)
Result.Success(user)
} catch (e: Exception) {
Result.Failure(e)
}
}
// ViewModel
var userResult by mutableStateOf<Result<User>?>(null)
private set
fun getUser(uid: String) {
viewModelScope.launch {
userResult = Result.Loading // set initial Loading state
userResult = repository.getUser(uid) // update the state again on receiving the response
}
}
So basically, on the snackbar action button, I want to Retry API call if user click on Retry.
I have used core MVVM architecture with Flow. I even used Flow between Viewmodel and view as well. Please note that I was already using livedata between view and ViewModel, but now the requirement has been changed and I have to use Flow only. Also I'm not using and shared or state flow, that is not required.
Code:
Fragment:
private fun apiCall() {
viewModel.fetchUserReviewData()
}
private fun setObservers() {
lifecycleScope.launch {
viewModel.userReviewData?.collect {
LogUtils.d("Hello it: " + it.code)
setLoadingState(it.state)
when (it.status) {
Resource.Status.ERROR -> showErrorSnackBarLayout(-1, it.message, {
// Retry action button logic
viewModel.userReviewData = null
apiCall()
})
}
}
}
Viewmodel:
var userReviewData: Flow<Resource<ReviewResponse>>? = emptyFlow<Resource<ReviewResponse>>()
fun fetchUserReviewData() {
LogUtils.d("Hello fetchUserReviewData: " + userReviewData)
userReviewData = flow {
emit(Resource.loading(true))
repository.getUserReviewData().collect {
emit(it)
}
}
}
EDIT in ViewModel:
// var userReviewData = MutableStateFlow<Resource<ReviewResponse>>(Resource.loading(false))
var userReviewData = MutableSharedFlow<Resource<ReviewResponse>>()
fun fetchUserReviewData() {
viewModelScope.launch {
userReviewData.emit(Resource.loading(true))
repository.getUserReviewData().collect {
userReviewData.emit(it)
}
}
}
override fun onCreate() {}
}
EDIT in Activity:
private fun setObservers() {
lifecycleScope.launchWhenStarted {
viewModel.userReviewData.collect {
setLoadingState(it.state)
when (it.status) {
Resource.Status.SUCCESS ->
if (it.data != null) {
val reviewResponse: ReviewResponse = it.data
if (!AppUtils.isNull(reviewResponse)) {
setReviewData(reviewResponse.data)
}
}
Resource.Status.ERROR -> showErrorSnackBarLayout(it.code, it.message) {
viewModel.fetchUserReviewData()
}
}
}
}
}
Now, I have only single doubt, should I use state one or shared one? I saw Phillip Lackener video and understood the difference, but still thinking what to use!
The thing is we only support Portrait orientation, but what in future requirement comes? In that case I think I have to use state one so that it can survive configuration changes! Don't know what to do!
Because of the single responsibility principle, the ViewModel alone should be updating its flow to show the latest requested data, rather than having to cancel the ongoing request and resubscribe to a new one from the Fragment side.
Here is one way you could do it. Use a MutableSharedFlow for triggering fetch requests and flatMapLatest to restart the downstream flow on a new request.
A Channel could also be used as a trigger, but it's a little more concise with MutableSharedFlow.
//In ViewModel
private val fetchRequest = MutableSharedFlow<Unit>(replay = 1, BufferOverflow.DROP_OLDEST)
var userReviewData = fetchRequest.flatMapLatest {
flow {
emit(Resource.loading(true))
emitAll(repository.getUserReviewData())
}
}.shareIn(viewModelScope, SharingStarted.WhlieSubscribed(5000), 1)
fun fetchUserReviewData() {
LogUtils.d("Hello fetchUserReviewData: " + userReviewData)
fetchRequest.tryEmit(Unit)
}
Your existing Fragment code above should work with this, but you no longer need the ?. null-safe call since the flow is not nullable.
However, if the coroutine does anything to views, you should use viewLifecycle.lifecycleScope instead of just lifecycleScope.
I have a huge project and I need to refactor code to LiveData (not Flow). I have an Order and states in ViewModel. I cannot receive this Order in Activity when I observe it. How can I do this? This is my View Model:
private var _basicModel: MutableLiveData<OrderUiState> = MutableLiveData()
val basicModel: LiveData<OrderUiState> get() = _basicModel
sealed class OrderUiState {
object Loading : OrderUiState()
data class OrderFail(val message: String) : OrderUiState()
data class OrderSuccess(val order: Order) : OrderUiState()
}
fun getOrder(orderId: String) {
viewModelScope.launch {
_basicModel.value = OrderUiState.Loading
getOrderUseCase.execute(orderId, { order ->
_basicModel.value = OrderUiState.OrderSuccess(order)
}
}
And now I cannot to get to Order, when I have Succes Sate. My code want from me in Activity order, but I thought, that whan it is success, there it will be, but isn't?
viewModel.basicModel.observe(this) { order ->
when(order){
OrderViewModel.OrderUiState.OrderSuccess(here he want from me order... )
}
}
Can I get to order from this code?
You can do it this way:
viewModel.basicModel.observe(this) { uiState ->
when(uiState) {
is OrderViewModel.OrderUiState.OrderSuccess -> {
val order = uiState.order
// Use the order here
}
}
}
Main Activity:
btLogin.setOnClickListener {
hideKeyboard()
if (etUsername.text.toString().isEmpty() || etPassword.text.toString().isEmpty()) {
etPasswordInput.error = resources.getString(R.string.main_enter_user_data)
} else if (viewModel.userLogin(
etUsername.text.toString(),
etPassword.text.toString(),
cbRememberMe.isChecked
)
) {
//val user: Array<String> = viewModel.getUserLoginData()
val intent: Intent = Intent(this, ProfilePageActivity::class.java)
intent.putExtra("EXTRA_USERNAME", "111111")
intent.putExtra("EXTRA_PASSWORD", "222555")
startActivity(intent)
finish()
}
else{
etPasswordInput.error = resources.getString(R.string.main_wrong_username_or_password)
}
}
MainActivityViewModel:
class MainActivityViewModel : ViewModel() {
fun userLogin(username: String, password: String, remember: Boolean): Boolean {
return Repository.userLogin(username, password, remember)
}
fun getUserLoginData(): Array<String> {
val array: Array<String> = arrayOf(
Repository.getStoredUsername(),
Repository.getStoredPassword()
)
return array
}
}
Repository:
fun userLogin(username: String, password: String, remember: Boolean): Boolean {
var response = false
var responseLogin = ""
runBlocking {
job_login = CoroutineScope(IO).launch {
responseLogin = HUC.httpRequest_Profile(username, password.md5())
if (responseLogin.contains(loginSuccess)) {
if (remember) {
preferences.saveUser(
username,
password,
password.md5()
)
}
response = true
}
}
job_login!!.join()
}
return response
}
This is how I am trying to implement User Login and it is working properly, BUT, it is blocking my UI thread until HTTP request is finished which results in a freeze right after a button is pressed which is not very good for user experience.
Here is what I am looking to do:
User pressed a button
Small progress bar appears meaning that the information is being processed
Data is sent for processing, meanwhile all animations on UI are finished, keyboard is hidden and etc.
Response from data processing is then being sent back to MainActivity
MainActivity is then processing the response and from then it either displays the error message or transfers to UserProfile
This is how I am trying to implement all this and the only thing I am having hard time implement is making it run without UI being blocked, help me understand how to achieve this
I was thinking that the right way to go about this would be a MutableLiveData with a Boolean value that would have a listener on MainActivity and inside a viewModel it would be changed depending on the response value from Repository, but maybe there is some other solution to this that I have not considered