WIP manejo de errores y reintentos

This commit is contained in:
Erik Cavazos 2023-09-10 13:17:50 -06:00
parent 761a076e52
commit 003ab3ea5d
25 changed files with 475 additions and 200 deletions

View File

@ -40,12 +40,7 @@ class AuthRepositoryImpl @Inject constructor(
return@flow
}
when(res.code()){
401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError))
404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError))
500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError))
}
emit(Resource.Error(Resource.Error.mapErrorCode(res.code())))
} catch (_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}

View File

@ -3,7 +3,26 @@ package com.isolaatti.common
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
class ErrorMessageViewModel : ViewModel() {
val error: MutableLiveData<Resource.Error.ErrorType> = MutableLiveData()
val error: MutableLiveData<Resource.Error.ErrorType?> = MutableLiveData()
private val _retry: MutableSharedFlow<Boolean> = MutableSharedFlow()
val retry: SharedFlow<Boolean> get() = _retry.asSharedFlow()
private var handledCount = 0
suspend fun askRetry() {
_retry.emit(false)
handledCount = 0
_retry.emit(true)
}
suspend fun handleRetry() {
val subscribers = _retry.subscriptionCount.value
if(handledCount >= subscribers) {
_retry.emit(false)
}
}
}

View File

@ -20,6 +20,9 @@ import com.isolaatti.home.HomeActivity
import com.isolaatti.login.LogInActivity
import com.isolaatti.utils.Resource
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@AndroidEntryPoint
abstract class IsolaattiBaseActivity : AppCompatActivity() {
@ -27,9 +30,10 @@ abstract class IsolaattiBaseActivity : AppCompatActivity() {
val errorViewModel: ErrorMessageViewModel by viewModels()
private var snackbarNetworkStatus: Snackbar? = null
private var snackbarError: Snackbar? = null
private var connectionHasBeenLost: Boolean = false
private val errorObserver: Observer<Resource.Error.ErrorType> = Observer {
private val errorObserver: Observer<Resource.Error.ErrorType?> = Observer {
when(it) {
Resource.Error.ErrorType.AuthError -> showReAuthDialog()
Resource.Error.ErrorType.NetworkError -> showNetworkErrorMessage()
@ -38,6 +42,7 @@ abstract class IsolaattiBaseActivity : AppCompatActivity() {
Resource.Error.ErrorType.OtherError -> showUnknownErrorMessage()
else -> {}
}
errorViewModel.error.postValue(null)
}
private val connectivityObserver: Observer<Boolean> = Observer { networkAvailable ->
@ -52,21 +57,22 @@ abstract class IsolaattiBaseActivity : AppCompatActivity() {
snackbarNetworkStatus = Snackbar.make(view, R.string.network_conn_restored, Snackbar.LENGTH_SHORT)
snackbarNetworkStatus?.show()
connectionHasBeenLost = false
CoroutineScope(Dispatchers.Default).launch {
errorViewModel.askRetry()
}
}
}
private val signInActivityResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
if(activityResult.resultCode == Activity.RESULT_OK) {
onRetry()
CoroutineScope(Dispatchers.Default).launch {
errorViewModel.handleRetry()
}
}
}
/**
* This method is called when a refresh should be performed. For example,
* when sign in flow is started completed from here, it is needed to know
* when it is complete.
*/
abstract fun onRetry()
private val onAcceptReAuthClick = DialogInterface.OnClickListener { _, _ ->
signInActivityResult.launch(Intent(this, LogInActivity::class.java))
@ -78,22 +84,67 @@ abstract class IsolaattiBaseActivity : AppCompatActivity() {
.setPositiveButton(R.string.accept, onAcceptReAuthClick)
.setNegativeButton(R.string.close, null)
.show()
errorViewModel.error.postValue(null)
}
private fun showNetworkErrorMessage() {
Toast.makeText(this, R.string.network_error, Toast.LENGTH_SHORT).show()
val view: View = window.decorView.findViewById(android.R.id.content) ?: return
snackbarError = Snackbar.make(view, R.string.network_error, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.retry) {
CoroutineScope(Dispatchers.Default).launch {
errorViewModel.askRetry()
}
errorViewModel.error.postValue(null)
}
snackbarError?.show()
}
private fun showServerErrorMessage() {
val view: View = window.decorView.findViewById(android.R.id.content) ?: return
snackbarError = Snackbar.make(view, R.string.server_error, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.retry) {
CoroutineScope(Dispatchers.Default).launch {
errorViewModel.askRetry()
}
errorViewModel.error.postValue(null)
}
snackbarError?.show()
}
private fun showNotFoundErrorMessage() {
val view: View = window.decorView.findViewById(android.R.id.content) ?: return
snackbarError = Snackbar.make(view, R.string.not_found, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.retry) {
CoroutineScope(Dispatchers.Default).launch {
errorViewModel.askRetry()
}
errorViewModel.error.postValue(null)
}
snackbarError?.show()
}
private fun showUnknownErrorMessage() {
val view: View = window.decorView.findViewById(android.R.id.content) ?: return
snackbarError = Snackbar.make(view, R.string.unknown_error, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.retry) {
CoroutineScope(Dispatchers.Default).launch {
errorViewModel.askRetry()
}
errorViewModel.error.postValue(null)
}
snackbarError?.show()
}
override fun onCreate(savedInstanceState: Bundle?) {

View File

@ -2,44 +2,64 @@ package com.isolaatti.followers.data
import com.isolaatti.followers.data.remote.FollowersApi
import com.isolaatti.followers.domain.FollowersRepository
import com.isolaatti.profile.data.remote.ProfileListItemDto
import com.isolaatti.profile.domain.entity.ProfileListItem
import com.isolaatti.utils.IntIdentificationWrapper
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse
import javax.inject.Inject
class FollowersRepositoryImpl @Inject constructor(private val followersApi: FollowersApi) : FollowersRepository {
override fun getFollowersOfUser(userId: Int, after: Int): Flow<List<ProfileListItem>> = flow {
override fun getFollowersOfUser(userId: Int, after: Int): Flow<Resource<List<ProfileListItem>>> = flow {
try {
val response = followersApi.getFollowersOfUser(userId, after).awaitResponse()
if(response.isSuccessful) {
response.body()?.let { emit(response.body()!!.map { ProfileListItem.fromDto(it) }) }
response.body()?.let { emit(Resource.Success(response.body()!!.map { ProfileListItem.fromDto(it) })) }
} else {
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
}
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
}
override fun getFollowingsOfUser(userId: Int, after: Int): Flow<List<ProfileListItem>> = flow {
override fun getFollowingsOfUser(userId: Int, after: Int): Flow<Resource<List<ProfileListItem>>> = flow {
try {
val response = followersApi.getFollowingsOfUser(userId, after).awaitResponse()
if(response.isSuccessful) {
response.body()?.let { emit(response.body()!!.map { ProfileListItem.fromDto(it) }) }
response.body()?.let { emit(Resource.Success(response.body()!!.map { ProfileListItem.fromDto(it) })) }
} else {
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
}
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
}
override fun followUser(userId: Int): Flow<Boolean> = flow {
override fun followUser(userId: Int): Flow<Resource<Boolean>> = flow {
try {
val response = followersApi.followUser(IntIdentificationWrapper(userId)).awaitResponse()
if(response.isSuccessful) {
emit(true)
emit(Resource.Success(true))
} else {
emit(false)
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
}
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
}
override fun unfollowUser(userId: Int): Flow<Boolean> = flow {
override fun unfollowUser(userId: Int): Flow<Resource<Boolean>> = flow {
try {
val response = followersApi.unfollowUser(IntIdentificationWrapper(userId)).awaitResponse()
if(response.isSuccessful) {
emit(true)
emit(Resource.Success(true))
} else {
emit(false)
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
}
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
}
}

View File

@ -1,14 +0,0 @@
package com.isolaatti.followers.domain
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class FollowUserUseCase @Inject constructor(private val followersRepository: FollowersRepository) {
fun follow(userId: Int): Flow<Boolean> {
return followersRepository.followUser(userId)
}
fun unfollow(userId: Int): Flow<Boolean> {
return followersRepository.unfollowUser(userId)
}
}

View File

@ -1,11 +1,12 @@
package com.isolaatti.followers.domain
import com.isolaatti.profile.domain.entity.ProfileListItem
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
interface FollowersRepository {
fun getFollowersOfUser(userId: Int, after: Int): Flow<List<ProfileListItem>>
fun getFollowingsOfUser(userId: Int, after: Int): Flow<List<ProfileListItem>>
fun followUser(userId: Int): Flow<Boolean>
fun unfollowUser(userId: Int): Flow<Boolean>
fun getFollowersOfUser(userId: Int, after: Int): Flow<Resource<List<ProfileListItem>>>
fun getFollowingsOfUser(userId: Int, after: Int): Flow<Resource<List<ProfileListItem>>>
fun followUser(userId: Int): Flow<Resource<Boolean>>
fun unfollowUser(userId: Int): Flow<Resource<Boolean>>
}

View File

@ -4,9 +4,9 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.isolaatti.followers.domain.FollowUserUseCase
import com.isolaatti.followers.domain.FollowersRepository
import com.isolaatti.profile.domain.entity.ProfileListItem
import com.isolaatti.utils.Resource
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
@ -16,7 +16,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class FollowersViewModel @Inject constructor(private val followersRepository: FollowersRepository, private val followUserUseCase: FollowUserUseCase) : ViewModel() {
class FollowersViewModel @Inject constructor(private val followersRepository: FollowersRepository) : ViewModel() {
var userId: Int = 0
private val followersList: MutableList<ProfileListItem> = mutableListOf()
@ -29,6 +29,19 @@ class FollowersViewModel @Inject constructor(private val followersRepository: Fo
val followers: LiveData<List<ProfileListItem>> get() = _followers
val followings: LiveData<List<ProfileListItem>> get() = _followings
private val toRetry: MutableList<Runnable> = mutableListOf()
// runs the lists of "Runnable" one by one and clears list. After this is executed,
// caller should report as handled
fun retry() {
toRetry.forEach {
it.run()
}
toRetry.clear()
}
private fun getFollowersLastId(): Int {
if(followersList.isEmpty()) {
@ -53,11 +66,23 @@ class FollowersViewModel @Inject constructor(private val followersRepository: Fo
viewModelScope.launch {
followersRepository.getFollowersOfUser(userId, getFollowersLastId()).onEach {
followersList.addAll(it)
_followers.postValue(followersList)
}.flowOn(Dispatchers.IO).launchIn(this)
when(it) {
is Resource.Success -> {
if(it.data != null) {
followersList.addAll(it.data)
}
_followers.postValue(followersList)
}
is Resource.Error -> {
toRetry.add {
fetchFollowers()
}
}
is Resource.Loading -> {}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
fun fetchFollowings() {
@ -67,8 +92,20 @@ class FollowersViewModel @Inject constructor(private val followersRepository: Fo
viewModelScope.launch {
followersRepository.getFollowingsOfUser(userId, getFollowingsLastId()).onEach {
followingsList.addAll(it)
when(it) {
is Resource.Error -> {
toRetry.add {
fetchFollowings()
}
}
is Resource.Loading -> {}
is Resource.Success -> {
if(it.data != null) {
followingsList.addAll(it.data)
_followings.postValue(followingsList)
}
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
@ -96,10 +133,20 @@ class FollowersViewModel @Inject constructor(private val followersRepository: Fo
replaceOnLists(user)
viewModelScope.launch {
followUserUseCase.follow(user.id).onEach {
followersRepository.followUser(user.id).onEach {
when(it) {
is Resource.Error -> {
toRetry.add {
followUser(user)
}
}
is Resource.Loading -> {}
is Resource.Success -> {
user.following = true
user.updatingFollowState = false
replaceOnLists(user)
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
@ -110,10 +157,21 @@ class FollowersViewModel @Inject constructor(private val followersRepository: Fo
replaceOnLists(user)
viewModelScope.launch {
followUserUseCase.unfollow(user.id).onEach {
followersRepository.unfollowUser(user.id).onEach {
when(it) {
is Resource.Error -> {
toRetry.add {
unfollowUser(user)
}
}
is Resource.Loading -> {}
is Resource.Success -> {
user.following = false
user.updatingFollowState = false
replaceOnLists(user)
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}

View File

@ -11,7 +11,10 @@ import android.widget.TextView
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.isolaatti.BuildConfig
import com.isolaatti.R
@ -45,6 +48,7 @@ import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute
import io.noties.markwon.linkify.LinkifyPlugin
import kotlinx.coroutines.launch
@AndroidEntryPoint
class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
@ -57,7 +61,6 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
private val errorViewModel: ErrorMessageViewModel by activityViewModels()
private val viewModel: FeedViewModel by activityViewModels()
val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels()
private var currentUserId = 0
private lateinit var viewBinding: FragmentFeedBinding
@ -220,9 +223,19 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
viewModel.errorLoading.observe(viewLifecycleOwner) {
errorViewModel.error.postValue(it)
}
viewModel.getProfile()
optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
errorViewModel.retry.collect {
viewModel.retry()
errorViewModel.handleRetry()
}
}
}
}
override fun onLiked(postId: Long) = viewModel.likePost(postId)

View File

@ -15,10 +15,6 @@ import com.isolaatti.home.presentation.FeedViewModel
class HomeActivity : IsolaattiBaseActivity() {
private lateinit var viewBinding: ActivityHomeBinding
private val feedViewModel: FeedViewModel by viewModels()
override fun onRetry() {
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@ -25,6 +25,19 @@ class FeedViewModel @Inject constructor(
private val postsRepository: PostsRepository
) : PostListingViewModelBase() {
private val toRetry: MutableList<Runnable> = mutableListOf()
// runs the lists of "Runnable" one by one and clears list. After this is executed,
// caller should report as handled
fun retry() {
toRetry.forEach {
it.run()
}
toRetry.clear()
}
override fun getFeed(refresh: Boolean) {
viewModelScope.launch {
if (refresh) {
@ -49,7 +62,10 @@ class FeedViewModel @Inject constructor(
}
is Resource.Error -> {
//errorLoading.postValue(feedDtoResource.errorType)
errorLoading.postValue(listResource.errorType)
toRetry.add {
getFeed(refresh)
}
}
}
@ -67,15 +83,24 @@ class FeedViewModel @Inject constructor(
authRepository.getUserId().onEach { userId ->
userId?.let {
getProfileUseCase(userId).onEach { profile ->
if (profile is Resource.Success) {
when(profile) {
is Resource.Error -> {
errorLoading.postValue(profile.errorType)
toRetry.add {
getProfile()
}
}
is Resource.Loading -> {}
is Resource.Success -> {
profile.data?.let { _userProfile.postValue(it) }
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
}

View File

@ -21,9 +21,6 @@ class PostViewerActivity : IsolaattiBaseActivity() {
private lateinit var binding: ActivityPostViewerBinding
private var postId: Long? = null
override fun onRetry() {
TODO("Not yet implemented")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@ -14,21 +14,36 @@ import javax.inject.Inject
class CommentsRepositoryImpl @Inject constructor(private val commentsApi: CommentsApi) :
CommentsRepository {
override fun getComments(postId: Long, lastId: Long): Flow<MutableList<Comment>> = flow {
override fun getComments(postId: Long, lastId: Long): Flow<Resource<MutableList<Comment>>> = flow {
try {
emit(Resource.Loading())
val response = commentsApi.getCommentsOfPosts(postId, lastId, 15).awaitResponse()
if(response.isSuccessful){
response.body()?.let { emit(Comment.fromCommentsDto(it).toMutableList()) }
response.body()?.let { emit(Resource.Success(Comment.fromCommentsDto(it).toMutableList())) }
} else {
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
}
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
}
override fun getComment(commentId: Long): Flow<CommentDto> = flow {
override fun getComment(commentId: Long): Flow<Resource<CommentDto>> = flow {
try {
emit(Resource.Loading())
val response = commentsApi.getComment(commentId).awaitResponse()
if(response.isSuccessful) {
response.body()?.let { emit(it) }
response.body()?.let { emit(Resource.Success(it)) }
} else {
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
}
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
}
override fun postComment(content: String, audioId: String?, postId: Long): Flow<Resource<Comment>> = flow {
try {
emit(Resource.Loading())
val commentToPostDto = CommentToPostDto(content, audioId)
val response = commentsApi.postComment(postId, commentToPostDto).awaitResponse()
@ -38,11 +53,17 @@ class CommentsRepositoryImpl @Inject constructor(private val commentsApi: Commen
emit(Resource.Success(Comment.fromCommentDto(responseBody)))
return@flow
}
} else {
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
}
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
emit(Resource.Error())
}
override fun editComment(commentId: Long, content: String, audioId: String?): Flow<Resource<Comment>> = flow {
try {
emit(Resource.Loading())
val commentToPostDto = CommentToPostDto(content, audioId)
val response = commentsApi.editComment(commentId, commentToPostDto).awaitResponse()
@ -53,8 +74,12 @@ class CommentsRepositoryImpl @Inject constructor(private val commentsApi: Commen
emit(Resource.Success(Comment.fromCommentDto(responseBody)))
return@flow
}
} else {
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
}
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
emit(Resource.Error())
}

View File

@ -6,8 +6,8 @@ import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
interface CommentsRepository {
fun getComments(postId: Long, lastId: Long): Flow<MutableList<Comment>>
fun getComment(commentId: Long): Flow<CommentDto>
fun getComments(postId: Long, lastId: Long): Flow<Resource<MutableList<Comment>>>
fun getComment(commentId: Long): Flow<Resource<CommentDto>>
fun postComment(content: String, audioId: String?, postId: Long): Flow<Resource<Comment>>
fun editComment(commentId: Long, content: String, audioId: String?): Flow<Resource<Comment>>
fun deleteComment(commentId: Long): Flow<Resource<Boolean>>

View File

@ -2,10 +2,11 @@ package com.isolaatti.posting.comments.domain.use_case
import com.isolaatti.posting.comments.domain.CommentsRepository
import com.isolaatti.posting.comments.domain.model.Comment
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetComments @Inject constructor(private val commentsRepository: CommentsRepository) {
operator fun invoke(postId: Long, lastId: Long? = null): Flow<MutableList<Comment>> =
operator fun invoke(postId: Long, lastId: Long? = null): Flow<Resource<MutableList<Comment>>> =
commentsRepository.getComments(postId, lastId ?: 0)
}

View File

@ -34,7 +34,20 @@ class CommentsViewModel @Inject constructor(
val noMoreContent: MutableLiveData<Boolean?> = MutableLiveData()
val commentToEdit: MutableLiveData<Comment?> = MutableLiveData()
val finishedEditingComment: MutableLiveData<Boolean?> = MutableLiveData()
val error: MutableLiveData<Boolean> = MutableLiveData(false)
val error: MutableLiveData<Resource.Error.ErrorType?> = MutableLiveData()
private val toRetry: MutableList<Runnable> = mutableListOf()
// runs the lists of "Runnable" one by one and clears list. After this is executed,
// caller should report as handled
fun retry() {
toRetry.forEach {
it.run()
}
toRetry.clear()
}
/**
* postId to query comments for. First page will be fetched when set.
@ -54,17 +67,30 @@ class CommentsViewModel @Inject constructor(
commentsList.clear()
}
getComments(postId, lastId).onEach {
when(it) {
is Resource.Error -> {
error.postValue(it.errorType)
toRetry.add {
getContent(refresh)
}
}
is Resource.Loading -> {}
is Resource.Success -> {
val eventType =
if ((commentsList.isNotEmpty())) UpdateEvent.UpdateType.COMMENT_PAGE_ADDED_BOTTOM else UpdateEvent.UpdateType.REFRESH
commentsList.addAll(it)
if(it.data == null) {
return@onEach
}
commentsList.addAll(it.data)
_comments.postValue(Pair(commentsList, UpdateEvent(eventType, null)))
if (it.isEmpty()) {
if (it.data.isEmpty()) {
noMoreContent.postValue(true)
}
if (it.isNotEmpty()) {
lastId = it.last().id
if (it.data.isNotEmpty()) {
lastId = it.data.last().id
}
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
@ -84,7 +110,13 @@ class CommentsViewModel @Inject constructor(
is Resource.Error -> {
commentPosted.postValue(false)
error.postValue(true)
error.postValue(it.errorType)
// this is the original call, put to retry
toRetry.add {
postComment(content)
}
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
@ -111,7 +143,11 @@ class CommentsViewModel @Inject constructor(
}
is Resource.Error -> {
error.postValue(true)
error.postValue(commentResource.errorType)
toRetry.add {
editComment(newContent)
}
}
is Resource.Loading -> {}
@ -129,7 +165,11 @@ class CommentsViewModel @Inject constructor(
deleteCommentUseCase(commentId).onEach {
when(it) {
is Resource.Error -> {
error.postValue(true)
error.postValue(it.errorType)
toRetry.add {
deleteComment(commentId)
}
}
is Resource.Loading -> {}
is Resource.Success -> {

View File

@ -10,7 +10,10 @@ import androidx.core.view.doOnLayout
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
@ -19,6 +22,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.isolaatti.R
import com.isolaatti.common.Dialogs
import com.isolaatti.common.ErrorMessageViewModel
import com.isolaatti.databinding.BottomSheetPostCommentsBinding
import com.isolaatti.posting.comments.domain.model.Comment
import com.isolaatti.posting.comments.presentation.CommentsRecyclerViewAdapter
@ -38,6 +42,8 @@ import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute
import io.noties.markwon.linkify.LinkifyPlugin
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@AndroidEntryPoint
class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedCallback {
@ -45,8 +51,8 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
private lateinit var viewBinding: BottomSheetPostCommentsBinding
val viewModel: CommentsViewModel by viewModels()
private lateinit var adapter: CommentsRecyclerViewAdapter
val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels()
private val errorViewModel: ErrorMessageViewModel by activityViewModels()
private val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels()
private val optionsObserver: Observer<OptionClicked?> = Observer { optionClicked ->
if(optionClicked?.callerId == CALLER_ID) {
@ -117,10 +123,7 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
viewModel.commentPosted.observe(viewLifecycleOwner, commentPostedObserver)
viewModel.commentToEdit.observe(viewLifecycleOwner, commentToEditObserver)
viewModel.error.observe(viewLifecycleOwner) {
if(it == true) {
Toast.makeText(requireContext(), "error", Toast.LENGTH_SHORT).show()
viewModel.error.postValue(null)
}
errorViewModel.error.postValue(it)
}
optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver)
@ -200,6 +203,18 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
// even if no change event is triggered
viewBinding.submitCommentButton.isEnabled = !viewBinding.newCommentTextField.editText?.text.isNullOrBlank()
// things to retry when user taps "Retry"
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
errorViewModel.retry.collect {
viewModel.retry()
errorViewModel.handleRetry()
}
}
}
setObservers()
setListeners()
}

View File

@ -83,10 +83,7 @@ class EditCommentDialogFragment : DialogFragment() {
}
viewModel.error.observe(viewLifecycleOwner) {
if(it) {
Toast.makeText(requireContext(), "error", Toast.LENGTH_SHORT).show()
viewModel.error.postValue(true)
}
viewModel.error.postValue(it)
}
viewModel.finishedEditingComment.observe(viewLifecycleOwner) {

View File

@ -14,6 +14,7 @@ import com.isolaatti.posting.posts.domain.entity.Post
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse
import javax.inject.Inject
class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, private val postApi: PostApi) : PostsRepository {
@ -40,17 +41,12 @@ class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, pr
emit(Resource.Loading())
try {
val gson = Gson()
val result = feedsApi.postsOfUser(userId, 20, lastId, olderFirst, gson.toJson(filter)).execute()
if(result.isSuccessful) {
emit(Resource.Success(result.body()?.let { Post.fromFeedDto(it) }))
val response = feedsApi.postsOfUser(userId, 20, lastId, olderFirst, gson.toJson(filter)).awaitResponse()
if(response.isSuccessful) {
emit(Resource.Success(response.body()?.let { Post.fromFeedDto(it) }))
return@flow
}
when(result.code()) {
401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError))
404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError))
500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError))
}
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
@ -59,17 +55,12 @@ class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, pr
override fun makePost(createPostDto: CreatePostDto): Flow<Resource<FeedDto.PostDto>> = flow {
emit(Resource.Loading())
try {
val result = postApi.makePost(createPostDto).execute()
val result = postApi.makePost(createPostDto).awaitResponse()
if(result.isSuccessful) {
emit(Resource.Success(result.body()))
return@flow
}
when(result.code()) {
401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError))
404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError))
500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError))
}
emit(Resource.Error(Resource.Error.mapErrorCode(result.code())))
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
@ -78,17 +69,12 @@ class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, pr
override fun editPost(editPostDto: EditPostDto): Flow<Resource<FeedDto.PostDto>> = flow {
emit(Resource.Loading())
try {
val result = postApi.editPost(editPostDto).execute()
val result = postApi.editPost(editPostDto).awaitResponse()
if(result.isSuccessful) {
emit(Resource.Success(result.body()))
return@flow
}
when(result.code()) {
401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError))
404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError))
500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError))
}
emit(Resource.Error(Resource.Error.mapErrorCode(result.code())))
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
@ -102,12 +88,7 @@ class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, pr
emit(Resource.Success(result.body()))
return@flow
}
when(result.code()) {
401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError))
404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError))
500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError))
}
emit(Resource.Error(Resource.Error.mapErrorCode(result.code())))
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
@ -121,12 +102,7 @@ class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, pr
emit(Resource.Success(result.body()))
return@flow
}
when(result.code()) {
401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError))
404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError))
500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError))
}
emit(Resource.Error(Resource.Error.mapErrorCode(result.code())))
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}

View File

@ -6,11 +6,15 @@ import android.os.Bundle
import android.view.View
import androidx.activity.viewModels
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.isolaatti.R
import com.isolaatti.common.IsolaattiBaseActivity
import com.isolaatti.databinding.ActivityCreatePostBinding
import com.isolaatti.posting.posts.presentation.CreatePostViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint
class CreatePostActivity : IsolaattiBaseActivity() {
@ -34,13 +38,6 @@ class CreatePostActivity : IsolaattiBaseActivity() {
val viewModel: CreatePostViewModel by viewModels()
var mode: Int = EXTRA_MODE_CREATE
var postId: Long = 0L
override fun onRetry() {
if(mode == EXTRA_MODE_EDIT && postId != 0L) {
viewModel.editDiscussion(postId, binding.filledTextField.editText?.text.toString())
} else {
viewModel.postDiscussion(binding.filledTextField.editText?.text.toString())
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -62,9 +59,24 @@ class CreatePostActivity : IsolaattiBaseActivity() {
setupUI()
setListeners()
setObservers()
setContentView(binding.root)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
errorViewModel.retry.collect {
if(!it) {
return@collect
}
if(mode == EXTRA_MODE_EDIT && postId != 0L) {
viewModel.editDiscussion(postId, binding.filledTextField.editText?.text.toString())
} else {
viewModel.postDiscussion(binding.filledTextField.editText?.text.toString())
}
}
}
}
}
private fun setupUI() {

View File

@ -6,24 +6,18 @@ import com.isolaatti.profile.domain.ProfileRepository
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.await
import retrofit2.awaitResponse
import javax.inject.Inject
class ProfileRepositoryImpl @Inject constructor(private val profileApi: ProfileApi) : ProfileRepository {
override fun getProfile(userId: Int): Flow<Resource<UserProfileDto>> = flow {
try {
val result = profileApi.userProfile(userId).execute()
val result = profileApi.userProfile(userId).awaitResponse()
if(result.isSuccessful) {
emit(Resource.Success(result.body()))
return@flow
}
when(result.code()) {
401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError))
404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError))
500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError))
}
emit(Resource.Error(Resource.Error.mapErrorCode(result.code())))
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}

View File

@ -35,10 +35,31 @@ class ProfileViewModel @Inject constructor(private val getProfileUseCase: GetPro
val followingState: MutableLiveData<FollowingState> = MutableLiveData()
private val toRetry: MutableList<Runnable> = mutableListOf()
// runs the lists of "Runnable" one by one and clears list. After this is executed,
// caller should report as handled
fun retry() {
toRetry.forEach {
it.run()
}
toRetry.clear()
}
fun getProfile() {
viewModelScope.launch {
getProfileUseCase(profileId).onEach {
if(it is Resource.Success) {
when(it) {
is Resource.Error -> {
errorLoading.postValue(it.errorType)
toRetry.add {
getProfile()
}
}
is Resource.Loading -> {}
is Resource.Success -> {
_profile.postValue(it.data!!)
followingState.postValue(
it.data.let {user->
@ -51,6 +72,7 @@ class ProfileViewModel @Inject constructor(private val getProfileUseCase: GetPro
}
)
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
@ -75,6 +97,9 @@ class ProfileViewModel @Inject constructor(private val getProfileUseCase: GetPro
is Resource.Error -> {
errorLoading.postValue(feedDtoResource.errorType)
toRetry.add {
getFeed(refresh)
}
}
}
}.flowOn(Dispatchers.IO).launchIn(this)

View File

@ -24,6 +24,7 @@ import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener
import com.google.android.material.tabs.TabLayoutMediator
import com.isolaatti.BuildConfig
import com.isolaatti.R
import com.isolaatti.common.IsolaattiBaseActivity
import com.isolaatti.databinding.ActivityProfileBinding
import com.isolaatti.posting.common.domain.OnUserInteractedCallback
import com.isolaatti.posting.common.domain.OnUserInteractedWithPostCallback
@ -42,7 +43,7 @@ import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAb
import io.noties.markwon.linkify.LinkifyPlugin
@AndroidEntryPoint
class ProfileActivity : FragmentActivity() {
class ProfileActivity : IsolaattiBaseActivity() {
lateinit var viewBinding: ActivityProfileBinding

View File

@ -9,7 +9,10 @@ import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.isolaatti.BuildConfig
@ -43,6 +46,7 @@ import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute
import io.noties.markwon.linkify.LinkifyPlugin
import kotlinx.coroutines.launch
@AndroidEntryPoint
class ProfileMainFragment : Fragment() {
@ -306,5 +310,16 @@ class ProfileMainFragment : Fragment() {
bind()
setObservers()
getData()
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
errorViewModel.retry.collect {
viewModel.retry()
errorViewModel.handleRetry()
}
}
}
}
}

View File

@ -7,5 +7,15 @@ sealed class Resource<T> {
enum class ErrorType {
NetworkError, AuthError, NotFoundError, ServerError, OtherError
}
companion object {
fun mapErrorCode(errorCode: Int): ErrorType {
return when(errorCode) {
401 -> ErrorType.AuthError
404 -> ErrorType.NotFoundError
505 -> ErrorType.ServerError
else -> ErrorType.OtherError
}
}
}
}
}

View File

@ -77,4 +77,7 @@
<string name="edit_comment">Edit comment</string>
<string name="network_conn_lost">Network connection lost</string>
<string name="network_conn_restored">Network connection restored</string>
<string name="retry">Retry</string>
<string name="not_found">The resource you are trying to load could not be found</string>
<string name="unknown_error">An unkwnow error occurred</string>
</resources>