From 1540d8ec2a0d0a7eb1ec045e96b563c2218d3701 Mon Sep 17 00:00:00 2001 From: erik-everardo Date: Thu, 25 Apr 2024 00:20:32 -0600 Subject: [PATCH] agrego ProfileListingFragment: pantalla para mostrar lista de usuarios de varios origenes --- .../presentation/FollowersViewPagerAdapter.kt | 7 +- .../followers/ui/MainFollowersFragment.kt | 8 +- .../presentation/PostInfoViewPagerAdapter.kt | 3 +- .../presentation/ProfileListingViewModel.kt | 226 ++++++++++++++++++ .../ui/ProfileListingFragment.kt | 130 ++++++++++ 5 files changed, 364 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/isolaatti/profile/profile_listing/presentation/ProfileListingViewModel.kt create mode 100644 app/src/main/java/com/isolaatti/profile/profile_listing/ui/ProfileListingFragment.kt diff --git a/app/src/main/java/com/isolaatti/followers/presentation/FollowersViewPagerAdapter.kt b/app/src/main/java/com/isolaatti/followers/presentation/FollowersViewPagerAdapter.kt index 68b3978..4052ab7 100644 --- a/app/src/main/java/com/isolaatti/followers/presentation/FollowersViewPagerAdapter.kt +++ b/app/src/main/java/com/isolaatti/followers/presentation/FollowersViewPagerAdapter.kt @@ -4,15 +4,16 @@ import androidx.fragment.app.Fragment import androidx.viewpager2.adapter.FragmentStateAdapter import com.isolaatti.followers.ui.FollowersFragment import com.isolaatti.followers.ui.FollowingFragment +import com.isolaatti.profile.profile_listing.ui.ProfileListingFragment -class FollowersViewPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { +class FollowersViewPagerAdapter(fragment: Fragment, private val userId: Int) : FragmentStateAdapter(fragment) { override fun getItemCount(): Int = 2 override fun createFragment(position: Int): Fragment { if(position == 0) { - return FollowersFragment() + return ProfileListingFragment.getInstanceForFollowers(userId) } - return FollowingFragment() + return ProfileListingFragment.getInstanceForFollowing(userId) } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/followers/ui/MainFollowersFragment.kt b/app/src/main/java/com/isolaatti/followers/ui/MainFollowersFragment.kt index f1d554c..11e6d5f 100644 --- a/app/src/main/java/com/isolaatti/followers/ui/MainFollowersFragment.kt +++ b/app/src/main/java/com/isolaatti/followers/ui/MainFollowersFragment.kt @@ -19,7 +19,6 @@ import dagger.hilt.android.AndroidEntryPoint class MainFollowersFragment : Fragment() { private lateinit var binding: FragmentFollowersMainBinding - private val viewModel: FollowersViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, @@ -33,12 +32,9 @@ class MainFollowersFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val userId = arguments?.getInt(ARGUMENT_USER_ID) ?: 0 - arguments?.getInt(ARGUMENT_USER_ID)?.let { - viewModel.userId = it - } - - binding.viewPagerFollowersMain.adapter = FollowersViewPagerAdapter(this) + binding.viewPagerFollowersMain.adapter = FollowersViewPagerAdapter(this, userId) TabLayoutMediator(binding.tabLayoutFollowers, binding.viewPagerFollowersMain) { tab, position -> when(position) { 0 -> tab.text = getText(R.string.followers) diff --git a/app/src/main/java/com/isolaatti/posting/posts/presentation/PostInfoViewPagerAdapter.kt b/app/src/main/java/com/isolaatti/posting/posts/presentation/PostInfoViewPagerAdapter.kt index fbd285b..c9b8e0f 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/presentation/PostInfoViewPagerAdapter.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/presentation/PostInfoViewPagerAdapter.kt @@ -5,12 +5,13 @@ import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter import com.isolaatti.posting.posts.ui.PostLikesFragment import com.isolaatti.posting.posts.ui.PostVersionsFragment +import com.isolaatti.profile.profile_listing.ui.ProfileListingFragment class PostInfoViewPagerAdapter(fragmentActivity: FragmentActivity, private val postId: Long) : FragmentStateAdapter(fragmentActivity) { override fun getItemCount(): Int = 2 override fun createFragment(position: Int): Fragment = if(position == 0) { - PostLikesFragment.getInstance(postId) + ProfileListingFragment.getInstanceForPostLikes(postId) } else{ PostVersionsFragment.getInstance(postId) } diff --git a/app/src/main/java/com/isolaatti/profile/profile_listing/presentation/ProfileListingViewModel.kt b/app/src/main/java/com/isolaatti/profile/profile_listing/presentation/ProfileListingViewModel.kt new file mode 100644 index 0000000..cc22d14 --- /dev/null +++ b/app/src/main/java/com/isolaatti/profile/profile_listing/presentation/ProfileListingViewModel.kt @@ -0,0 +1,226 @@ +package com.isolaatti.profile.profile_listing.presentation + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.isolaatti.common.ListUpdateEvent +import com.isolaatti.common.UpdateEvent +import com.isolaatti.followers.domain.FollowersRepository +import com.isolaatti.posting.posts.domain.PostsRepository +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 +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ProfileListingViewModel @Inject constructor( + private val followersRepository: FollowersRepository, + private val postsRepository: PostsRepository +): ViewModel() { + // one of these values must be set from fragment, depending on what action will be performed + var userId: Int = 0 // for followers and followings + var postId: Long = 0 // for "liked by" + + private var usersList: List = listOf() + + + private val _users: MutableLiveData, UpdateEvent>> = MutableLiveData() + + val users: LiveData, UpdateEvent>> get() = _users + + private val toRetry: MutableList = 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 getUsersLastId(): Int { + if(usersList.isEmpty()) { + return 0 + } + + return usersList.last().id + } + + /** + * fetchFollowers for user previously set. userId must be set beforehand + */ + fun fetchFollowers(refresh: Boolean = false) { + if(userId <= 0) { + throw IllegalStateException("userId must not be 0. Be sure to set userId on view model before calling fetchFollowers method") + } + + if(refresh) { + usersList = mutableListOf() + } + + val updateListEvent = if(refresh) ListUpdateEvent.Refresh else ListUpdateEvent.ItemsAdded + + viewModelScope.launch { + followersRepository.getFollowersOfUser(userId, getUsersLastId()).onEach { + when(it) { + is Resource.Success -> { + if(it.data != null) { + val prevCount = usersList.count() + usersList += it.data + _users.postValue(Pair(usersList, UpdateEvent(updateListEvent, arrayOf(prevCount)))) + } + } + is Resource.Error -> { + toRetry.add { + fetchFollowers() + } + } + is Resource.Loading -> {} + } + }.flowOn(Dispatchers.IO).launchIn(this) + } + } + + /** + * fetchFollowings for user previously set. userId must be set beforehand + */ + fun fetchFollowings(refresh: Boolean = false) { + if(userId <= 0) { + throw IllegalStateException("userId must not be 0. Be sure to set userId on view model before calling fetchFollowings method") + } + + if(refresh) { + usersList = mutableListOf() + } + + val updateListEvent = if(refresh) ListUpdateEvent.Refresh else ListUpdateEvent.ItemsAdded + + viewModelScope.launch { + followersRepository.getFollowingsOfUser(userId, getUsersLastId()).onEach { + when(it) { + is Resource.Error -> { + toRetry.add { + fetchFollowings() + } + } + is Resource.Loading -> {} + is Resource.Success -> { + if(it.data != null) { + val prevCount = usersList.count() + usersList += it.data + _users.postValue(Pair(usersList, UpdateEvent(updateListEvent, arrayOf(prevCount)))) + } + } + } + }.flowOn(Dispatchers.IO).launchIn(this) + } + } + + /** + * fetchPostLikedBy for post previously set. postId must be set beforehand + */ + fun fetchPostLikedBy(refresh: Boolean = false) { + if(postId == 0L) { + throw IllegalStateException("postId must not be 0. Be sure to set postId on view model before calling fetchPostLikedBy method") + } + + if(refresh) { + usersList = mutableListOf() + } + + val updateListEvent = if(refresh) ListUpdateEvent.Refresh else ListUpdateEvent.ItemsAdded + + viewModelScope.launch { + postsRepository.getUsersLikedPost(postId).onEach { resource -> + when(resource) { + is Resource.Error -> { + toRetry.add { + fetchFollowings() + } + } + is Resource.Loading -> {} + is Resource.Success -> { + if(resource.data != null) { + val prevCount = usersList.count() + usersList += resource.data + _users.postValue(Pair(usersList, UpdateEvent(updateListEvent, arrayOf(prevCount)))) + } + } + } + }.flowOn(Dispatchers.IO).launchIn(this) + } + } + + private fun replaceOnLists(user: ProfileListItem) { + val userIndex = usersList.indexOf(user) + + + if(userIndex >= 0) { + usersList = usersList.toMutableList().apply { + set(userIndex, user) + } + + _users.postValue(Pair(usersList, UpdateEvent(ListUpdateEvent.ItemUpdated, arrayOf(userIndex)))) + } + } + + fun followUser(user: ProfileListItem) { + user.updatingFollowState = true + + replaceOnLists(user) + + viewModelScope.launch { + 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) + } + } + + + fun unfollowUser(user: ProfileListItem) { + user.updatingFollowState = true + + replaceOnLists(user) + + viewModelScope.launch { + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/profile/profile_listing/ui/ProfileListingFragment.kt b/app/src/main/java/com/isolaatti/profile/profile_listing/ui/ProfileListingFragment.kt new file mode 100644 index 0000000..3fa5d5e --- /dev/null +++ b/app/src/main/java/com/isolaatti/profile/profile_listing/ui/ProfileListingFragment.kt @@ -0,0 +1,130 @@ +package com.isolaatti.profile.profile_listing.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.isolaatti.common.UserItemCallback +import com.isolaatti.common.UserListRecyclerViewAdapter +import com.isolaatti.databinding.FragmentUserListBinding +import com.isolaatti.profile.domain.entity.ProfileListItem +import com.isolaatti.profile.profile_listing.presentation.ProfileListingViewModel +import com.isolaatti.profile.ui.ProfileActivity +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ProfileListingFragment : Fragment(), UserItemCallback { + + private val viewModel: ProfileListingViewModel by viewModels() + private lateinit var binding: FragmentUserListBinding + private var mode: Int = 0 + + private lateinit var adapter: UserListRecyclerViewAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentUserListBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + mode = arguments?.getInt(ARG_MODE) ?: 0 + when(mode) { + FOLLOWING, FOLLOWERS -> viewModel.userId = arguments?.getInt(ARG_USER_ID) ?: 0 + POST_LIKES -> viewModel.postId = arguments?.getLong(ARG_POST_ID) ?: 0 + } + loadData() + + } + + private fun loadData(refresh: Boolean = false) { + when(mode) { + FOLLOWING -> viewModel.fetchFollowings(refresh) + FOLLOWERS -> viewModel.fetchFollowers(refresh) + POST_LIKES -> viewModel.fetchPostLikedBy(refresh) + BROWSE_PROFILES -> {} + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + adapter = UserListRecyclerViewAdapter(this) + binding.recyclerUsers.adapter = adapter + binding.recyclerUsers.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) + + viewModel.users.observe(viewLifecycleOwner) { (list, updateEvent) -> + adapter.updateData(list, updateEvent) + binding.swipeToRefresh.isRefreshing = false + } + + binding.swipeToRefresh.setOnRefreshListener { + loadData(refresh = true) + } + } + + companion object { + private const val ARG_MODE = "mode" + private const val ARG_USER_ID = "userId" + private const val ARG_POST_ID = "postId" + const val FOLLOWERS = 1 + const val FOLLOWING = 2 + const val POST_LIKES = 3 + const val BROWSE_PROFILES = 4 + + fun getInstanceForFollowers(userId: Int): ProfileListingFragment { + return ProfileListingFragment().apply { + arguments = Bundle().apply { + putInt(ARG_MODE, FOLLOWERS) + putInt(ARG_USER_ID, userId) + } + } + } + + fun getInstanceForFollowing(userId: Int): ProfileListingFragment { + return ProfileListingFragment().apply { + arguments = Bundle().apply { + putInt(ARG_MODE, FOLLOWING) + putInt(ARG_USER_ID, userId) + } + } + } + + fun getInstanceForPostLikes(postId: Long): ProfileListingFragment { + return ProfileListingFragment().apply { + arguments = Bundle().apply { + putInt(ARG_MODE, POST_LIKES) + putLong(ARG_POST_ID, postId) + } + } + } + + fun getInstanceForBrowseProfile(): ProfileListingFragment { + return ProfileListingFragment().apply { + arguments = Bundle().apply { + putInt(ARG_MODE, BROWSE_PROFILES) + } + } + } + } + + override fun itemClick(userId: Int) { + ProfileActivity.startActivity(requireContext(), userId) + } + + override fun followButtonClick( + user: ProfileListItem, + action: UserItemCallback.FollowButtonAction + ) { + when(action) { + UserItemCallback.FollowButtonAction.Follow -> viewModel.followUser(user) + UserItemCallback.FollowButtonAction.Unfollow -> viewModel.unfollowUser(user) + } + } +} \ No newline at end of file