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 return@flow
} }
when(res.code()){ emit(Resource.Error(Resource.Error.mapErrorCode(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))
}
} catch (_: Exception) { } catch (_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError)) emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
} }

View File

@ -3,7 +3,26 @@ package com.isolaatti.common
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
class ErrorMessageViewModel : ViewModel() { 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.login.LogInActivity
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@AndroidEntryPoint @AndroidEntryPoint
abstract class IsolaattiBaseActivity : AppCompatActivity() { abstract class IsolaattiBaseActivity : AppCompatActivity() {
@ -27,9 +30,10 @@ abstract class IsolaattiBaseActivity : AppCompatActivity() {
val errorViewModel: ErrorMessageViewModel by viewModels() val errorViewModel: ErrorMessageViewModel by viewModels()
private var snackbarNetworkStatus: Snackbar? = null private var snackbarNetworkStatus: Snackbar? = null
private var snackbarError: Snackbar? = null
private var connectionHasBeenLost: Boolean = false private var connectionHasBeenLost: Boolean = false
private val errorObserver: Observer<Resource.Error.ErrorType> = Observer { private val errorObserver: Observer<Resource.Error.ErrorType?> = Observer {
when(it) { when(it) {
Resource.Error.ErrorType.AuthError -> showReAuthDialog() Resource.Error.ErrorType.AuthError -> showReAuthDialog()
Resource.Error.ErrorType.NetworkError -> showNetworkErrorMessage() Resource.Error.ErrorType.NetworkError -> showNetworkErrorMessage()
@ -38,6 +42,7 @@ abstract class IsolaattiBaseActivity : AppCompatActivity() {
Resource.Error.ErrorType.OtherError -> showUnknownErrorMessage() Resource.Error.ErrorType.OtherError -> showUnknownErrorMessage()
else -> {} else -> {}
} }
errorViewModel.error.postValue(null)
} }
private val connectivityObserver: Observer<Boolean> = Observer { networkAvailable -> 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 = Snackbar.make(view, R.string.network_conn_restored, Snackbar.LENGTH_SHORT)
snackbarNetworkStatus?.show() snackbarNetworkStatus?.show()
connectionHasBeenLost = false connectionHasBeenLost = false
CoroutineScope(Dispatchers.Default).launch {
errorViewModel.askRetry()
}
} }
} }
private val signInActivityResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> private val signInActivityResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
if(activityResult.resultCode == Activity.RESULT_OK) { 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 { _, _ -> private val onAcceptReAuthClick = DialogInterface.OnClickListener { _, _ ->
signInActivityResult.launch(Intent(this, LogInActivity::class.java)) signInActivityResult.launch(Intent(this, LogInActivity::class.java))
@ -78,22 +84,67 @@ abstract class IsolaattiBaseActivity : AppCompatActivity() {
.setPositiveButton(R.string.accept, onAcceptReAuthClick) .setPositiveButton(R.string.accept, onAcceptReAuthClick)
.setNegativeButton(R.string.close, null) .setNegativeButton(R.string.close, null)
.show() .show()
errorViewModel.error.postValue(null)
} }
private fun showNetworkErrorMessage() { 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() { 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() { 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() { 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?) { 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.data.remote.FollowersApi
import com.isolaatti.followers.domain.FollowersRepository import com.isolaatti.followers.domain.FollowersRepository
import com.isolaatti.profile.data.remote.ProfileListItemDto
import com.isolaatti.profile.domain.entity.ProfileListItem import com.isolaatti.profile.domain.entity.ProfileListItem
import com.isolaatti.utils.IntIdentificationWrapper import com.isolaatti.utils.IntIdentificationWrapper
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse import retrofit2.awaitResponse
import javax.inject.Inject import javax.inject.Inject
class FollowersRepositoryImpl @Inject constructor(private val followersApi: FollowersApi) : FollowersRepository { 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 {
val response = followersApi.getFollowersOfUser(userId, after).awaitResponse() try {
if(response.isSuccessful) { val response = followersApi.getFollowersOfUser(userId, after).awaitResponse()
response.body()?.let { emit(response.body()!!.map { ProfileListItem.fromDto(it) }) } if(response.isSuccessful) {
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 {
val response = followersApi.getFollowingsOfUser(userId, after).awaitResponse() try {
if(response.isSuccessful) { val response = followersApi.getFollowingsOfUser(userId, after).awaitResponse()
response.body()?.let { emit(response.body()!!.map { ProfileListItem.fromDto(it) }) } if(response.isSuccessful) {
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 {
val response = followersApi.followUser(IntIdentificationWrapper(userId)).awaitResponse() try {
if(response.isSuccessful) { val response = followersApi.followUser(IntIdentificationWrapper(userId)).awaitResponse()
emit(true) if(response.isSuccessful) {
} else { emit(Resource.Success(true))
emit(false) } else {
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 {
val response = followersApi.unfollowUser(IntIdentificationWrapper(userId)).awaitResponse() try {
if(response.isSuccessful) { val response = followersApi.unfollowUser(IntIdentificationWrapper(userId)).awaitResponse()
emit(true) if(response.isSuccessful) {
} else { emit(Resource.Success(true))
emit(false) } else {
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 package com.isolaatti.followers.domain
import com.isolaatti.profile.domain.entity.ProfileListItem import com.isolaatti.profile.domain.entity.ProfileListItem
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface FollowersRepository { interface FollowersRepository {
fun getFollowersOfUser(userId: Int, after: Int): Flow<List<ProfileListItem>> fun getFollowersOfUser(userId: Int, after: Int): Flow<Resource<List<ProfileListItem>>>
fun getFollowingsOfUser(userId: Int, after: Int): Flow<List<ProfileListItem>> fun getFollowingsOfUser(userId: Int, after: Int): Flow<Resource<List<ProfileListItem>>>
fun followUser(userId: Int): Flow<Boolean> fun followUser(userId: Int): Flow<Resource<Boolean>>
fun unfollowUser(userId: Int): Flow<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.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.isolaatti.followers.domain.FollowUserUseCase
import com.isolaatti.followers.domain.FollowersRepository import com.isolaatti.followers.domain.FollowersRepository
import com.isolaatti.profile.domain.entity.ProfileListItem import com.isolaatti.profile.domain.entity.ProfileListItem
import com.isolaatti.utils.Resource
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@ -16,7 +16,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @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 var userId: Int = 0
private val followersList: MutableList<ProfileListItem> = mutableListOf() 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 followers: LiveData<List<ProfileListItem>> get() = _followers
val followings: LiveData<List<ProfileListItem>> get() = _followings 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 { private fun getFollowersLastId(): Int {
if(followersList.isEmpty()) { if(followersList.isEmpty()) {
@ -53,11 +66,23 @@ class FollowersViewModel @Inject constructor(private val followersRepository: Fo
viewModelScope.launch { viewModelScope.launch {
followersRepository.getFollowersOfUser(userId, getFollowersLastId()).onEach { followersRepository.getFollowersOfUser(userId, getFollowersLastId()).onEach {
followersList.addAll(it) when(it) {
_followers.postValue(followersList) 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) }.flowOn(Dispatchers.IO).launchIn(this)
} }
} }
fun fetchFollowings() { fun fetchFollowings() {
@ -67,8 +92,20 @@ class FollowersViewModel @Inject constructor(private val followersRepository: Fo
viewModelScope.launch { viewModelScope.launch {
followersRepository.getFollowingsOfUser(userId, getFollowingsLastId()).onEach { followersRepository.getFollowingsOfUser(userId, getFollowingsLastId()).onEach {
followingsList.addAll(it) when(it) {
_followings.postValue(followingsList) 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) }.flowOn(Dispatchers.IO).launchIn(this)
} }
} }
@ -96,10 +133,20 @@ class FollowersViewModel @Inject constructor(private val followersRepository: Fo
replaceOnLists(user) replaceOnLists(user)
viewModelScope.launch { viewModelScope.launch {
followUserUseCase.follow(user.id).onEach { followersRepository.followUser(user.id).onEach {
user.following = true when(it) {
user.updatingFollowState = false is Resource.Error -> {
replaceOnLists(user) toRetry.add {
followUser(user)
}
}
is Resource.Loading -> {}
is Resource.Success -> {
user.following = true
user.updatingFollowState = false
replaceOnLists(user)
}
}
}.flowOn(Dispatchers.IO).launchIn(this) }.flowOn(Dispatchers.IO).launchIn(this)
} }
} }
@ -110,10 +157,21 @@ class FollowersViewModel @Inject constructor(private val followersRepository: Fo
replaceOnLists(user) replaceOnLists(user)
viewModelScope.launch { viewModelScope.launch {
followUserUseCase.unfollow(user.id).onEach { followersRepository.unfollowUser(user.id).onEach {
user.following = false when(it) {
user.updatingFollowState = false is Resource.Error -> {
replaceOnLists(user) toRetry.add {
unfollowUser(user)
}
}
is Resource.Loading -> {}
is Resource.Success -> {
user.following = false
user.updatingFollowState = false
replaceOnLists(user)
}
}
}.flowOn(Dispatchers.IO).launchIn(this) }.flowOn(Dispatchers.IO).launchIn(this)
} }
} }

View File

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

View File

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

View File

@ -25,6 +25,19 @@ class FeedViewModel @Inject constructor(
private val postsRepository: PostsRepository private val postsRepository: PostsRepository
) : PostListingViewModelBase() { ) : 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) { override fun getFeed(refresh: Boolean) {
viewModelScope.launch { viewModelScope.launch {
if (refresh) { if (refresh) {
@ -49,7 +62,10 @@ class FeedViewModel @Inject constructor(
} }
is Resource.Error -> { 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 -> authRepository.getUserId().onEach { userId ->
userId?.let { userId?.let {
getProfileUseCase(userId).onEach { profile -> getProfileUseCase(userId).onEach { profile ->
if (profile is Resource.Success) {
profile.data?.let { _userProfile.postValue(it) }
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)
} }
}.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 lateinit var binding: ActivityPostViewerBinding
private var postId: Long? = null private var postId: Long? = null
override fun onRetry() {
TODO("Not yet implemented")
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@ -14,47 +14,72 @@ import javax.inject.Inject
class CommentsRepositoryImpl @Inject constructor(private val commentsApi: CommentsApi) : class CommentsRepositoryImpl @Inject constructor(private val commentsApi: CommentsApi) :
CommentsRepository { CommentsRepository {
override fun getComments(postId: Long, lastId: Long): Flow<MutableList<Comment>> = flow { override fun getComments(postId: Long, lastId: Long): Flow<Resource<MutableList<Comment>>> = flow {
val response = commentsApi.getCommentsOfPosts(postId, lastId, 15).awaitResponse() try {
if(response.isSuccessful){ emit(Resource.Loading())
response.body()?.let { emit(Comment.fromCommentsDto(it).toMutableList()) } val response = commentsApi.getCommentsOfPosts(postId, lastId, 15).awaitResponse()
if(response.isSuccessful){
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 {
val response = commentsApi.getComment(commentId).awaitResponse() try {
if(response.isSuccessful) { emit(Resource.Loading())
response.body()?.let { emit(it) } val response = commentsApi.getComment(commentId).awaitResponse()
if(response.isSuccessful) {
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 { override fun postComment(content: String, audioId: String?, postId: Long): Flow<Resource<Comment>> = flow {
emit(Resource.Loading()) try {
val commentToPostDto = CommentToPostDto(content, audioId) emit(Resource.Loading())
val response = commentsApi.postComment(postId, commentToPostDto).awaitResponse() val commentToPostDto = CommentToPostDto(content, audioId)
if(response.isSuccessful) { val response = commentsApi.postComment(postId, commentToPostDto).awaitResponse()
val responseBody = response.body() if(response.isSuccessful) {
if(responseBody != null) { val responseBody = response.body()
emit(Resource.Success(Comment.fromCommentDto(responseBody))) if(responseBody != null) {
return@flow 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 { override fun editComment(commentId: Long, content: String, audioId: String?): Flow<Resource<Comment>> = flow {
emit(Resource.Loading()) try {
val commentToPostDto = CommentToPostDto(content, audioId) emit(Resource.Loading())
val response = commentsApi.editComment(commentId, commentToPostDto).awaitResponse() val commentToPostDto = CommentToPostDto(content, audioId)
val response = commentsApi.editComment(commentId, commentToPostDto).awaitResponse()
if(response.isSuccessful) { if(response.isSuccessful) {
val responseBody = response.body() val responseBody = response.body()
if(responseBody != null) { if(responseBody != null) {
emit(Resource.Success(Comment.fromCommentDto(responseBody))) emit(Resource.Success(Comment.fromCommentDto(responseBody)))
return@flow 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 import kotlinx.coroutines.flow.Flow
interface CommentsRepository { interface CommentsRepository {
fun getComments(postId: Long, lastId: Long): Flow<MutableList<Comment>> fun getComments(postId: Long, lastId: Long): Flow<Resource<MutableList<Comment>>>
fun getComment(commentId: Long): Flow<CommentDto> fun getComment(commentId: Long): Flow<Resource<CommentDto>>
fun postComment(content: String, audioId: String?, postId: Long): Flow<Resource<Comment>> fun postComment(content: String, audioId: String?, postId: Long): Flow<Resource<Comment>>
fun editComment(commentId: Long, content: String, audioId: String?): Flow<Resource<Comment>> fun editComment(commentId: Long, content: String, audioId: String?): Flow<Resource<Comment>>
fun deleteComment(commentId: Long): Flow<Resource<Boolean>> 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.CommentsRepository
import com.isolaatti.posting.comments.domain.model.Comment import com.isolaatti.posting.comments.domain.model.Comment
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import javax.inject.Inject import javax.inject.Inject
class GetComments @Inject constructor(private val commentsRepository: CommentsRepository) { 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) commentsRepository.getComments(postId, lastId ?: 0)
} }

View File

@ -34,7 +34,20 @@ class CommentsViewModel @Inject constructor(
val noMoreContent: MutableLiveData<Boolean?> = MutableLiveData() val noMoreContent: MutableLiveData<Boolean?> = MutableLiveData()
val commentToEdit: MutableLiveData<Comment?> = MutableLiveData() val commentToEdit: MutableLiveData<Comment?> = MutableLiveData()
val finishedEditingComment: MutableLiveData<Boolean?> = 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. * postId to query comments for. First page will be fetched when set.
@ -54,17 +67,30 @@ class CommentsViewModel @Inject constructor(
commentsList.clear() commentsList.clear()
} }
getComments(postId, lastId).onEach { getComments(postId, lastId).onEach {
val eventType = when(it) {
if ((commentsList.isNotEmpty())) UpdateEvent.UpdateType.COMMENT_PAGE_ADDED_BOTTOM else UpdateEvent.UpdateType.REFRESH is Resource.Error -> {
commentsList.addAll(it) error.postValue(it.errorType)
_comments.postValue(Pair(commentsList, UpdateEvent(eventType, null))) toRetry.add {
if (it.isEmpty()) { getContent(refresh)
noMoreContent.postValue(true) }
}
is Resource.Loading -> {}
is Resource.Success -> {
val eventType =
if ((commentsList.isNotEmpty())) UpdateEvent.UpdateType.COMMENT_PAGE_ADDED_BOTTOM else UpdateEvent.UpdateType.REFRESH
if(it.data == null) {
return@onEach
}
commentsList.addAll(it.data)
_comments.postValue(Pair(commentsList, UpdateEvent(eventType, null)))
if (it.data.isEmpty()) {
noMoreContent.postValue(true)
}
if (it.data.isNotEmpty()) {
lastId = it.data.last().id
}
}
} }
if (it.isNotEmpty()) {
lastId = it.last().id
}
}.flowOn(Dispatchers.IO).launchIn(this) }.flowOn(Dispatchers.IO).launchIn(this)
} }
} }
@ -84,7 +110,13 @@ class CommentsViewModel @Inject constructor(
is Resource.Error -> { is Resource.Error -> {
commentPosted.postValue(false) 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) }.flowOn(Dispatchers.IO).launchIn(this)
@ -111,7 +143,11 @@ class CommentsViewModel @Inject constructor(
} }
is Resource.Error -> { is Resource.Error -> {
error.postValue(true) error.postValue(commentResource.errorType)
toRetry.add {
editComment(newContent)
}
} }
is Resource.Loading -> {} is Resource.Loading -> {}
@ -129,7 +165,11 @@ class CommentsViewModel @Inject constructor(
deleteCommentUseCase(commentId).onEach { deleteCommentUseCase(commentId).onEach {
when(it) { when(it) {
is Resource.Error -> { is Resource.Error -> {
error.postValue(true) error.postValue(it.errorType)
toRetry.add {
deleteComment(commentId)
}
} }
is Resource.Loading -> {} is Resource.Loading -> {}
is Resource.Success -> { is Resource.Success -> {

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import com.isolaatti.posting.posts.domain.entity.Post
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse
import javax.inject.Inject import javax.inject.Inject
class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, private val postApi: PostApi) : PostsRepository { 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()) emit(Resource.Loading())
try { try {
val gson = Gson() val gson = Gson()
val result = feedsApi.postsOfUser(userId, 20, lastId, olderFirst, gson.toJson(filter)).execute() val response = feedsApi.postsOfUser(userId, 20, lastId, olderFirst, gson.toJson(filter)).awaitResponse()
if(result.isSuccessful) { if(response.isSuccessful) {
emit(Resource.Success(result.body()?.let { Post.fromFeedDto(it) })) emit(Resource.Success(response.body()?.let { Post.fromFeedDto(it) }))
return@flow return@flow
} }
when(result.code()) { emit(Resource.Error(Resource.Error.mapErrorCode(response.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))
}
} catch(_: Exception) { } catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError)) 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 { override fun makePost(createPostDto: CreatePostDto): Flow<Resource<FeedDto.PostDto>> = flow {
emit(Resource.Loading()) emit(Resource.Loading())
try { try {
val result = postApi.makePost(createPostDto).execute() val result = postApi.makePost(createPostDto).awaitResponse()
if(result.isSuccessful) { if(result.isSuccessful) {
emit(Resource.Success(result.body())) emit(Resource.Success(result.body()))
return@flow return@flow
} }
when(result.code()) { emit(Resource.Error(Resource.Error.mapErrorCode(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))
}
} catch(_: Exception) { } catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError)) 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 { override fun editPost(editPostDto: EditPostDto): Flow<Resource<FeedDto.PostDto>> = flow {
emit(Resource.Loading()) emit(Resource.Loading())
try { try {
val result = postApi.editPost(editPostDto).execute() val result = postApi.editPost(editPostDto).awaitResponse()
if(result.isSuccessful) { if(result.isSuccessful) {
emit(Resource.Success(result.body())) emit(Resource.Success(result.body()))
return@flow return@flow
} }
when(result.code()) { emit(Resource.Error(Resource.Error.mapErrorCode(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))
}
} catch(_: Exception) { } catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError)) 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())) emit(Resource.Success(result.body()))
return@flow return@flow
} }
when(result.code()) { emit(Resource.Error(Resource.Error.mapErrorCode(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))
}
} catch(_: Exception) { } catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError)) 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())) emit(Resource.Success(result.body()))
return@flow return@flow
} }
when(result.code()) { emit(Resource.Error(Resource.Error.mapErrorCode(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))
}
} catch(_: Exception) { } catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError)) emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
} }

View File

@ -6,11 +6,15 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.common.IsolaattiBaseActivity import com.isolaatti.common.IsolaattiBaseActivity
import com.isolaatti.databinding.ActivityCreatePostBinding import com.isolaatti.databinding.ActivityCreatePostBinding
import com.isolaatti.posting.posts.presentation.CreatePostViewModel import com.isolaatti.posting.posts.presentation.CreatePostViewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint @AndroidEntryPoint
class CreatePostActivity : IsolaattiBaseActivity() { class CreatePostActivity : IsolaattiBaseActivity() {
@ -34,13 +38,6 @@ class CreatePostActivity : IsolaattiBaseActivity() {
val viewModel: CreatePostViewModel by viewModels() val viewModel: CreatePostViewModel by viewModels()
var mode: Int = EXTRA_MODE_CREATE var mode: Int = EXTRA_MODE_CREATE
var postId: Long = 0L 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -62,9 +59,24 @@ class CreatePostActivity : IsolaattiBaseActivity() {
setupUI() setupUI()
setListeners() setListeners()
setObservers() setObservers()
setContentView(binding.root) 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() { private fun setupUI() {

View File

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

View File

@ -35,21 +35,43 @@ class ProfileViewModel @Inject constructor(private val getProfileUseCase: GetPro
val followingState: MutableLiveData<FollowingState> = MutableLiveData() 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() { fun getProfile() {
viewModelScope.launch { viewModelScope.launch {
getProfileUseCase(profileId).onEach { getProfileUseCase(profileId).onEach {
if(it is Resource.Success) { when(it) {
_profile.postValue(it.data!!) is Resource.Error -> {
followingState.postValue( errorLoading.postValue(it.errorType)
it.data.let {user-> toRetry.add {
when { getProfile()
user.followingThisUser && user.thisUserIsFollowingMe -> FollowingState.MutuallyFollowing
user.followingThisUser -> FollowingState.FollowingThisUser
user.thisUserIsFollowingMe -> FollowingState.ThisUserIsFollowingMe
else -> FollowingState.NotMutuallyFollowing
}
} }
) }
is Resource.Loading -> {}
is Resource.Success -> {
_profile.postValue(it.data!!)
followingState.postValue(
it.data.let {user->
when {
user.followingThisUser && user.thisUserIsFollowingMe -> FollowingState.MutuallyFollowing
user.followingThisUser -> FollowingState.FollowingThisUser
user.thisUserIsFollowingMe -> FollowingState.ThisUserIsFollowingMe
else -> FollowingState.NotMutuallyFollowing
}
}
)
}
} }
}.flowOn(Dispatchers.IO).launchIn(this) }.flowOn(Dispatchers.IO).launchIn(this)
} }
@ -75,6 +97,9 @@ class ProfileViewModel @Inject constructor(private val getProfileUseCase: GetPro
is Resource.Error -> { is Resource.Error -> {
errorLoading.postValue(feedDtoResource.errorType) errorLoading.postValue(feedDtoResource.errorType)
toRetry.add {
getFeed(refresh)
}
} }
} }
}.flowOn(Dispatchers.IO).launchIn(this) }.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.google.android.material.tabs.TabLayoutMediator
import com.isolaatti.BuildConfig import com.isolaatti.BuildConfig
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.common.IsolaattiBaseActivity
import com.isolaatti.databinding.ActivityProfileBinding import com.isolaatti.databinding.ActivityProfileBinding
import com.isolaatti.posting.common.domain.OnUserInteractedCallback import com.isolaatti.posting.common.domain.OnUserInteractedCallback
import com.isolaatti.posting.common.domain.OnUserInteractedWithPostCallback import com.isolaatti.posting.common.domain.OnUserInteractedWithPostCallback
@ -42,7 +43,7 @@ import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAb
import io.noties.markwon.linkify.LinkifyPlugin import io.noties.markwon.linkify.LinkifyPlugin
@AndroidEntryPoint @AndroidEntryPoint
class ProfileActivity : FragmentActivity() { class ProfileActivity : IsolaattiBaseActivity() {
lateinit var viewBinding: ActivityProfileBinding lateinit var viewBinding: ActivityProfileBinding

View File

@ -9,7 +9,10 @@ import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.isolaatti.BuildConfig import com.isolaatti.BuildConfig
@ -43,6 +46,7 @@ import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute
import io.noties.markwon.linkify.LinkifyPlugin import io.noties.markwon.linkify.LinkifyPlugin
import kotlinx.coroutines.launch
@AndroidEntryPoint @AndroidEntryPoint
class ProfileMainFragment : Fragment() { class ProfileMainFragment : Fragment() {
@ -306,5 +310,16 @@ class ProfileMainFragment : Fragment() {
bind() bind()
setObservers() setObservers()
getData() 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 { enum class ErrorType {
NetworkError, AuthError, NotFoundError, ServerError, OtherError 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="edit_comment">Edit comment</string>
<string name="network_conn_lost">Network connection lost</string> <string name="network_conn_lost">Network connection lost</string>
<string name="network_conn_restored">Network connection restored</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> </resources>