I am creating an android application following the MVVM patron with the goal of retrieving data from a Firebase collection.
Before applying this pattern, I did proof of concept and I was able to retrieve data from the Firebase collection. But once I apply MVVM, I am not able to get the data from that collection, my screen does not show anything. I am not able to return the data from the repository to be painted on the screen.
This is my code:
Model:
data class PotatoesData(
val modifiedDate: String,
var potatoes: List<Potato>
)
data class Potato(
val type: String,
val site: String
)
State:
data class PotatoesState(
val isLoading: Boolean = false,
val potatoes: List<Potato> = emptyList(),
val error: String = ""
)
ModelView:
#HiltViewModel
class PotatoesViewModel #Inject constructor(
private val getPotatoesDataUseCase: GetPotatoesData
) : ViewModel() {
private val _state = mutableStateOf(PotatoesState())
val state: State<PotatoesState> = _state
init {
getPotatoes()
}
private fun getPotatoes() {
getPotatoesDataUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = PotatoesState(potatoes = result.data?.potatoes ?: emptyList())
}
is Resource.Error -> {
_state.value = PotatoesState(
error = result.message ?: "An unexpected error occurred"
)
}
is Resource.Loading -> {
_state.value = PotatoesState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
}
UseCase:
class GetPotatoesData #Inject constructor(
private val repository: PotatoRepository
) {
operator fun invoke(): Flow<Resource<PotatoesData>> = flow {
try {
emit(Resource.Loading())
val potatoes = repository.getPotatoesData()
emit(Resource.Success(potatoes))
} catch (e: IOException) {
emit(Resource.Error("Couldn't reach server. Check your internet connection."))
}
}
}
Repository implementation:
class PotatoRepositoryImpl : PotatoRepository {
override suspend fun getPotatoesData(): PotatoesData {
var potatoes = PotatoesData("TEST", emptyList())
FirestoreProvider.getLastPotatoes(
{ potatoesData ->
if (potatoesData != null) {
potatoes = potatoesData
}
},
{
potatoes
}
)
return potatoes
}
}
Firestore provider:
object FirestoreProvider {
private val incidentsRef = FirebaseFirestore.getInstance().collection(FirestoreCollection.POTATOES.key)
fun getLastPotatoes(
success: (potatoesData: PotatoesData?) -> Unit,
failure: () -> Unit
) {
val query: Query = orderBy(FirestoreField.CREATED_DATE, Query.Direction.DESCENDING).limit(1)
val querySnapshot: Task<QuerySnapshot> = query.get()
querySnapshot
.addOnSuccessListener {
if (!querySnapshot.result.isEmpty) {
val document = querySnapshot.result.documents[0]
val potatoesDataDB: PotatoesDataDto? = document.toObject(PotatoesDataDto::class.java)
potatoesDataDB?.let {
success(potatoesDataDB.toPotatoesData())
} ?: run {
success(null)
}
} else {
success(null)
}
}
.addOnFailureListener {
failure()
}
}
private fun orderBy(field: FirestoreField, direction: Query.Direction): Query {
return incidentsRef.orderBy(field.key, direction)
}
}
I am thankful for any kind of help! Thanks in advance!
I think the error is in the way of how you are handling Firestore callbacks. in FirestoreProvider: the callback will fire later than the function getLastPotatoes returns. Try to make that function suspend and use suspendCoroutine to wait for the callback and return it's result. It will look something like:
suspend fun getLastPotatoes() = suspendCoroutine <PotatoesData?> { continuation ->
val query: Query = orderBy(FirestoreField.CREATED_DATE, Query.Direction.DESCENDING).limit(1)
val querySnapshot: Task<QuerySnapshot> = query.get()
querySnapshot
.addOnSuccessListener {
if (!querySnapshot.result.isEmpty) {
val document = querySnapshot.result.documents[0]
val potatoesDataDB: PotatoesDataDto? = document.toObject(PotatoesDataDto::class.java)
potatoesDataDB?.let {
continuation.resume(potatoesDataDB.toPotatoesData())
} ?: run {
continuation.resume(null)
}
} else {
continuation.resume(null)
}
}
.addOnFailureListener {
continuation.resumeWithException(...)
}
}
suspendCoroutine suspends coroutine in which it executed until we decide to continue by calling appropriate methods - Continuation.resume....
In your PotatoRepositoryImpl:
override suspend fun getPotatoesData(): PotatoesData {
var potatoes = PotatoesData("TEST", emptyList())
try {
val potatoesData = FirestoreProvider.getLastPotatoes()
if (potatoesData != null) {
potatoes = potatoesData
}
} catch (e: Exception) {
// handle Exception
}
return potatoes
}
Related
I have some problem with handling network response. Loading and Success state work perfectly fine but Error state doesn't work. When i simulate an error then triggered Loading and Success state not an Error. My code below.
Repository class
fun getMoviesStream(genreId: String?, sortBy: String): Flow<PagingData<Result>> {
return Pager(
config = PagingConfig(pageSize = MOVIES_PAGE_SIZE, enablePlaceholders = false),
pagingSourceFactory = { MoviesPagingSource(api, genreId, sortBy) }
).flow
}
ViewModel class
private val _moviesResponse = MutableStateFlow<Resource<PagingData<Result>>>(Resource.Loading())
val moviesResponse = _moviesResponse.asStateFlow()
fun getMovies(genreId: String?, sortBy: String) {
viewModelScope.launch {
_moviesResponse.value = Resource.Loading()
moviesRepository.getMoviesStream(genreId, sortBy).cachedIn(viewModelScope)
.catch { throwable ->
_moviesResponse.value = Resource.Error(
throwable.localizedMessage ?: "An unexpected error occurred"
)
}
.collect { result ->
delay(500)
_moviesResponse.value = Resource.Success(result)
}
}
}
Fragment class
private fun requestMoviesData(genreId: String?, sortBy: String) {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
moviesViewModel.getMovies(genreId, sortBy)
moviesViewModel.moviesResponse.collect { response ->
when (response) {
is Resource.Error -> {
Toast.makeText(
requireContext(),
response.message,
Toast.LENGTH_SHORT
).show()
}
is Resource.Loading -> {
binding.spinnerLoading.visibility = View.VISIBLE
}
is Resource.Success -> {
binding.spinnerLoading.visibility = View.GONE
response.data?.let { moviesAdapter.submitData(it) }
}
}
}
}
}
}
Please check the flow catch operator documentation.
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/catch.html
This operator is transparent to exceptions that occur in downstream flow and does not catch exceptions that are thrown to cancel the flow.
I'm new to android and I'm developing a few applications for studying.
I've been trying to improve a code that I have but I got stuck in the following problem:
I'm creating a new user, validating it with Google Firebase. I managed to create a user normally but I'm not able to handle with one exception from the register moment which is the "FirebaseAuthUserCollisionException".
I created a class to handle a most of exceptions from email/password mistakes:
class AddUser(private val repository: UserRepository) {
#Throws(InvalidUserException::class)
suspend operator fun invoke(user: UserModel) {
if(user.email.isEmpty()) {
throw InvalidUserException("Email cannot be empty")
}
if(!Patterns.EMAIL_ADDRESS.matcher(user.email).matches()) {
throw InvalidUserException("Email is not valid")
}
if(user.password.length <= 5) {
throw InvalidUserException("Password should contain at least 6 characters")
}
if(user.password.isEmpty()) {
throw InvalidUserException("Password cannot be empty")
}
if(user.confirmPassword.isEmpty()) {
throw InvalidUserException("Confirm password cannot be empty")
}
if(user.password != user.confirmPassword) {
throw InvalidUserException("Passwords does not match")
}
repository.insert(user)
}
}
My repository:
class UserRepositoryImpl: UserRepository {
private var auth: FirebaseAuth = Firebase.auth
private var database: DatabaseReference = FirebaseDatabase.getInstance().getReference("users")
override suspend fun insert(user: UserModel) {
auth = FirebaseAuth.getInstance()
auth.createUserWithEmailAndPassword(user.email, user.password).addOnCompleteListener {
if(it.isSuccessful) {
database.child(user.id.toString()).setValue(user)
} else {
//exception here
}
}
}
}
When this function is triggered, it navigates to another fragment and toasts the successful message, which is incorrect because the exception happens.
Fragment:
private fun configEventFlow() = lifecycleScope.launch {
viewModel.eventFlow.collectLatest { event ->
when(event) {
is RegisterViewModel.UiEvent.ShowToast -> {
toast(event.message)
}
is RegisterViewModel.UiEvent.SaveUser -> {
val action = RegisterFragmentDirections.actionRegisterFragmentToMainFragment()
findNavController().navigate(action)
toast(getString(R.string.register_successfully))
}
}
}
}
private fun configUserRegistration() = with(binding) {
fabRegister.setOnClickListener {
val email = editRegisterEmail.text.toString()
viewModel.onEvent(RegisterUserEvents.EnteredEmail(email))
val password = editRegisterPassword.text.toString()
viewModel.onEvent(RegisterUserEvents.EnteredPassword(password))
val confirmPassword = editRegisterPasswordConfirm.text.toString()
viewModel.onEvent(RegisterUserEvents.EnteredConfirmPassword(confirmPassword))
viewModel.onEvent(RegisterUserEvents.SaveUser)
}
}
ViewModel:
#HiltViewModel
class RegisterViewModel #Inject constructor(private val useCases: UserUseCases): ViewModel() {
private val _email = MutableStateFlow<ResourceState<String>>(ResourceState.Empty())
private val email: StateFlow<ResourceState<String>> = _email
private val _password = MutableStateFlow<ResourceState<String>>(ResourceState.Empty())
private val password: StateFlow<ResourceState<String>> = _password
private val _confirmPassword = MutableStateFlow<ResourceState<String>>(ResourceState.Empty())
private val confirmPassword: StateFlow<ResourceState<String>> = _confirmPassword
private val _eventFlow = MutableSharedFlow<UiEvent>()
val eventFlow = _eventFlow.asSharedFlow()
fun onEvent(event: RegisterUserEvents) {
when(event) {
is RegisterUserEvents.EnteredEmail -> {
_email.value = ResourceState.Success(event.value)
}
is RegisterUserEvents.EnteredPassword -> {
_password.value = ResourceState.Success(event.value)
}
is RegisterUserEvents.EnteredConfirmPassword -> {
_confirmPassword.value = ResourceState.Success(event.value)
}
is RegisterUserEvents.SaveUser -> {
viewModelScope.launch {
try {
useCases.addUser(
UserModel(
id = System.currentTimeMillis().toInt(),
email = email.value.data!!,
password = password.value.data!!,
confirmPassword = confirmPassword.value.data!!
)
)
_eventFlow.emit(UiEvent.SaveUser)
} catch(e: InvalidUserException) {
_eventFlow.emit(UiEvent.ShowToast(message = e.message!!))
}
}
}
}
}
sealed class UiEvent {
data class ShowToast(val message: String): UiEvent()
object SaveUser: UiEvent()
}
}
Is there a way that I can manage this specific exception in this pattern? Even if I catch the exception there, the action is completed and my application follows at it was registered but in the database it does not occur because of the exception. Im sure that I'll have to face it again when login to handle specific exceptions from Firebase, which I cannot create this way but I have to receive them and display to the user.
Any suggestions??
Sorry if it's missing any content, tell me and I update asap.
Thanks in advance.
I have a isLoading state and I'm trying to show a CircularProgressIndicator when the value is true.
#Composable
fun ProductDetailScreen(
viewModel: ProductDetailViewModel = hiltViewModel()
) {
val productState = viewModel.productState.value
LazyColumn{
item {
if (productState.isLoading)
CircularProgressIndicator()
}
}
}
I'm using a Resource class for my API call results and in the repository I use this class to wrap my request result.
The problem is, although I'm returning Resource.Loading from the repository, the isLoading state is not being updated from ViewModel and the ProgressIndicator is not shown in my screen. What could be causing this behavior?
sealed class Resource<T>(
val data: T? = null,
val message: String? = null,
val errorType: ExceptionMapper.Type? = null
) {
class Success<T>(data: T?) : Resource<T>(data)
class Error<T>(message: String, errorType: ExceptionMapper.Type, data: T? = null) : Resource<T>(data, message, errorType)
class Loading<T>(isLoading: Boolean = true) : Resource<T>()
}
Repository:
override suspend fun getProductComments(productId: Int): Resource<List<Comment>> {
return try {
Resource.Loading<List<Comment>>()
delay(3000)
Resource.Success(apiService.getComments(productId))
} catch (t: Throwable) {
val mappedException = ExceptionMapper.map(t)
Resource.Error(message = t.message!!, errorType = mappedException.type)
}
}
ViewModel:
#HiltViewModel
class ProductDetailViewModel #Inject constructor(
state: SavedStateHandle,
private val productRepository: ProductRepository
) : ViewModel() {
private val passedProduct = state.get<Product>(EXTRA_KEY_DATA)
var productId = passedProduct?.id
var productState = mutableStateOf(ProductState())
private set
init {
getProductComments()
}
private fun getProductComments() {
viewModelScope.launch {
productId?.let { pId ->
when (val commentResult = productRepository.getProductComments(pId)) {
is Resource.Success -> {
commentResult.data?.let { comments ->
productState.value =
productState.value.copy(
comments = comments,
error = null,
isLoading = false
)
}
}
is Resource.Error -> {
productState.value = productState.value.copy(
isLoadFailed = true,
isLoading = false,
error = commentResult.message
)
}
is Resource.Loading -> {
productState.value = productState.value.copy(
isLoadFailed = false,
isLoading = true,
error = null
)
}
}
}
}
}
}
Your'e only checking this
is Resource.Loading -> {
...
}
when the repository returns, at this point its useless because when the call to getProductComments is done, it's already Resource.Success.
return try {
Resource.Loading<List<Comment>>() // you'll never get this value
delay(3000)
Resource.Success(apiService.getComments(productId))
So I'd suggest to update the ProductState before you call the repository
private fun getProductComments() {
productState.value = productState.value.copy(isLoading = true)
viewModelScope.launch {
...
...
or set isLoading to true as its initial state.
data class ProductState(
...
...
val isLoading : Boolean = true
...
)
I have this sealed class:
sealed class Resource<out T> {
object Loading: Resource<Nothing>()
data class Success<out T>(val data: T): Resource<T>()
data class Failure(val message: String): Resource<Nothing>()
}
In the repository class I have this function that deletes an item from an API:
override suspend fun deleteItem(id: String) = flow {
try {
emit(Resource.Loading)
emit(Resource.Success(itemsRef.document(id).delete().await()))
} catch (e: Exception) {
emit(Resource.Failure(e.message))
}
}
The result of the delete operation is Void?. Now, in the ViewModel class I declare:
val state = mutableStateOf<Resource<Void?>>(Success(null))
And update it when the delete completes:
fun deleteItem(id: String) {
viewModelScope.launch {
repo.deleteItem(id).collect { response ->
state.value = response
}
}
}
I have created a Card and inside onClick I have added:
IconButton(
onClick = viewModel.deleteItem(id),
)
Which actually deletes that item form database correctly. But I cannot track the result of the operation. I tried using:
when(val res = viewModel.state.value) {
is Resource.Loading -> Log.d(TAG, "Loading")
is Resource.Success -> Log.d(TAG, "Success")
is Resource.Failure -> Log.d(TAG, "Failure")
}
But only the case Loading is triggered. No success/failure at all. What can be wrong here? As it really acts like a synchronous operation.
I've tested your approach without a repository, and compose part looks totally fine:
var i = 0
#Composable
fun TestScreen(viewModel: TestViewModel = viewModel()) {
val state by viewModel.state
Text(
when (val stateSmartCast = state) {
is Resource.Failure -> "Failure ${stateSmartCast.message}"
Resource.Loading -> "Loading"
is Resource.Success -> "Success ${stateSmartCast.data}"
}
)
Button(onClick = {
viewModel.deleteItem(++i)
}) {
}
}
class TestViewModel : ViewModel() {
val state = mutableStateOf<Resource<Int>>(Resource.Success(i))
fun deleteItem(id: Int) {
viewModelScope.launch {
deleteItemInternal(id).collect { response ->
state.value = response
}
}
}
suspend fun deleteItemInternal(id: Int) = flow {
try {
emit(Resource.Loading)
delay(1000)
if (id % 3 == 0) {
throw IllegalStateException("error on third")
}
emit(Resource.Success(id))
} catch (e: Exception) {
emit(Resource.Failure(e.message ?: e.toString()))
}
}
}
So the the problem looks like in this line itemsRef.document(id).delete().await()), or in your connection to the repository.
Try collecting in the composable function:
val state = viewModel.state.collectAsState()
Then you can do: when (val res = viewModel.state.value){...}.
However I am sceptical about the deleteItem in the repository returning a flow. Do you really need such thing? You can always map stuff in the viewModel.
I am consuming a rest API that uses cursor based pagination to show some results. I am wondering if I can use Paging Library 3.0 to paginate it. I have been looking through some mediums and docs and can't seem to find a way to implement it. If any of you has come around any solution, I would be so happy to hear from it!
The api response pagination looks like this:
"paging": {
"previous": false,
"next": "https://api.acelerala.com/v1/orders/?store_id=4&after=xyz",
"cursors": {
"before": false,
"after": "xyz"
}
}
In kotlin, here example.
In Activity or somewhere:
viewModel.triggerGetMoreData("data").collectLatest {
mAdapter.submitData(it)
}
In viewModel:
fun triggerGetMoreData(data: String): Flow<PagingData<SampleData>> {
val request = ExampleRequest(data)
return exampleRepository.getMoreData(request).cachedIn(viewModelScope)
}
In Repository:
fun getMoreData(request: ExampleRequest): Flow<PagingData<ExampleData>> {
return Pager(
config = PagingConfig(
pageSize = 30,
enablePlaceholders = false
),
pagingSourceFactory = { ExamplePagingSource(service, request) }
).flow
}
and
class ExamplePagingSource (
private val service: ExampleService,
private val request: ExampleRequest): PagingSource<Int, ExampleData>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ExampleData> {
return try {
val pageIndex = params.key ?: 0
val request = request.copy(index = (request.pageNum.toInt() * pageIndex).toString())
when (val result = service.getMoreData(request)) { // call api
is NetworkResponse.Success -> {
val listData = result.body.items?.toData()?: listOf()
LoadResult.Page(
data = listData,
prevKey = if (pageIndex == 0) null else pageIndex - 1,
nextKey = if (listData.isEmpty()) null else pageIndex + 1
)
}
else -> LoadResult.Error(result.toError())
}
} catch (e: Exception) {
e.printStackTrace()
LoadResult.Error(e)
}
}
}
Thanks to #Đặng Anh Hào I was able to get on track. As my cursor is a String and not at Int, the Paging Source load function looks like this:
override suspend fun load(params: LoadParams<String>): LoadResult<String, Order> {
return try{
val response = service.getOrders(query,params.key?:"",10)
val nextKey = if(response.paging?.cursors?.after=="false") null else response.paging?.cursors?.after
val prevKey = if(response.paging?.cursors?.before=="false") null else response.paging?.cursors?.before
LoadResult.Page(response.data?.toOrderList()?:emptyList(),prevKey,nextKey)
}catch (exception: IOException) {
LoadResult.Error(exception)
} catch (exception: retrofit2.HttpException) {
LoadResult.Error(exception)
}
}
and the onrefreshkey looks like this:
override fun getRefreshKey(state: PagingState<String, Order>): String? {
return state.anchorPosition?.let {
state.closestItemToPosition(it)?.orderId
}
}
The repository method looks like this:
fun getOrdersPaginated(storeId: String): Flow<PagingData<Order>> {
return Pager(
config = PagingConfig(enablePlaceholders = false,pageSize = 10),
pagingSourceFactory = {PagingSource(apiService,storeId)}
).flow
}
And the View Model method is like this:
private val _pagedOrders = MutableLiveData<PagingData<Order>>()
val orders get() = _pagedOrders
private var currentQueryValue: String? = null
private var currentSearchResult: Flow<PagingData<Order>>? = null
fun getOrdersPaginated(storeId: String) {
viewModelScope.launch {
currentQueryValue = storeId
val newResult: Flow<PagingData<Order>> = repository.getOrdersPaginated(storeId)
.cachedIn(viewModelScope)
currentSearchResult = newResult
currentSearchResult!!.collect {
_pagedOrders.value = it
}
}
}
The activity calls the paging like this:
private var searchJob: Job? = null
private fun getOrders() {
viewModel.getOrdersPaginated(storeId)
}
private fun listenForChanges() {
viewModel.orders.observe(this, {
searchJob?.cancel()
searchJob = lifecycleScope.launch {
ordersAdapter.submitData(it)
}
})
}
And finally the adapter is the same as a ListAdapter, the only thing that changes is that it now extends PagingDataAdapter<Order, OrderAdapter.ViewHolder>(OrdersDiffer)
For a more detailed tutorial on how to do it, I read this codelab