From 35462dfe00dbe27e6ad19c8fc2c8b9b7ac33ff1b Mon Sep 17 00:00:00 2001 From: clfreville2 Date: Fri, 24 Mar 2023 14:28:49 +0100 Subject: [PATCH] Infinite scroll in the repository list --- .../iut/clfreville2/teaiswarm/MainActivity.kt | 43 +++----- .../{ => adapter}/RepositoryListAdapter.kt | 21 +++- .../fragment/RepositoryListFragment.kt | 104 ++++++++++++++++++ .../teaiswarm/network/GiteaService.kt | 7 +- app/src/main/res/layout/activity_main.xml | 27 +---- app/src/main/res/layout/repository_list.xml | 9 ++ 6 files changed, 152 insertions(+), 59 deletions(-) rename app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/{ => adapter}/RepositoryListAdapter.kt (79%) create mode 100644 app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/fragment/RepositoryListFragment.kt create mode 100644 app/src/main/res/layout/repository_list.xml diff --git a/app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/MainActivity.kt b/app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/MainActivity.kt index fb924be..d1a5d22 100644 --- a/app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/MainActivity.kt +++ b/app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/MainActivity.kt @@ -3,39 +3,26 @@ package fr.uca.iut.clfreville2.teaiswarm import android.content.Intent import androidx.appcompat.app.AppCompatActivity import android.os.Bundle -import android.widget.Button -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.RecyclerView +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory +import androidx.fragment.app.commit +import fr.uca.iut.clfreville2.teaiswarm.fragment.RepositoryListFragment import fr.uca.iut.clfreville2.teaiswarm.model.Repository -import fr.uca.iut.clfreville2.teaiswarm.network.GiteaService -import kotlinx.coroutines.launch -import kotlin.math.max const val REPOSITORY_OWNER = "repository_owner" const val REPOSITORY_NAME = "repository_name" class MainActivity : AppCompatActivity() { - private val service = GiteaService() - private lateinit var repositories: RecyclerView - private lateinit var previousButton: Button - private lateinit var nextButton: Button - private var currentPage = 1 - override fun onCreate(savedInstanceState: Bundle?) { + supportFragmentManager.fragmentFactory = RepositoryListFragmentFactory { repo -> + adapterOnClick(repo) + } super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - repositories = findViewById(R.id.repositories_view) - previousButton = findViewById(R.id.previous_repository_list) - nextButton = findViewById(R.id.next_repository_list) - updateList() - previousButton.setOnClickListener { - currentPage = max(currentPage - 1, 0) - updateList() - } - nextButton.setOnClickListener { - currentPage += 1 - updateList() + supportFragmentManager.commit { + setReorderingAllowed(true) + replace(R.id.fragment_container_view, RepositoryListFragment::class.java, null) } } @@ -46,12 +33,12 @@ class MainActivity : AppCompatActivity() { startActivity(intent) } - private fun updateList() { - lifecycleScope.launch { - val repos = service.listActiveRepositories("clement.freville2", currentPage) - repositories.adapter = RepositoryListAdapter(repos) { repo -> - adapterOnClick(repo) + class RepositoryListFragmentFactory(private val onClick: (Repository) -> Unit) : FragmentFactory() { + override fun instantiate(classLoader: ClassLoader, className: String): Fragment { + if (className == RepositoryListFragment::class.java.name) { + return RepositoryListFragment("clement.freville2", onClick); } + return super.instantiate(classLoader, className) } } } \ No newline at end of file diff --git a/app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/RepositoryListAdapter.kt b/app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/adapter/RepositoryListAdapter.kt similarity index 79% rename from app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/RepositoryListAdapter.kt rename to app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/adapter/RepositoryListAdapter.kt index 6d866bd..26c202c 100644 --- a/app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/RepositoryListAdapter.kt +++ b/app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/adapter/RepositoryListAdapter.kt @@ -1,14 +1,17 @@ -package fr.uca.iut.clfreville2.teaiswarm +package fr.uca.iut.clfreville2.teaiswarm.adapter import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import fr.uca.iut.clfreville2.teaiswarm.R import fr.uca.iut.clfreville2.teaiswarm.model.Repository -class RepositoryListAdapter(private val dataSet: List, private val onClick: (Repository) -> Unit) : - RecyclerView.Adapter() { +class RepositoryListAdapter(diffCallback: DiffUtil.ItemCallback, private val onClick: (Repository) -> Unit) : + PagingDataAdapter(diffCallback) { class ViewHolder(view: View, private val onClick: (Repository) -> Unit) : RecyclerView.ViewHolder(view) { private val repositoryNameView: TextView @@ -72,8 +75,14 @@ class RepositoryListAdapter(private val dataSet: List, private val o } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(dataSet[position]) + holder.bind(getItem(position)) } - override fun getItemCount() = dataSet.size -} \ No newline at end of file + object RepositoryComparator : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Repository, newItem: Repository): Boolean = + oldItem.owner == newItem.owner && oldItem.name == newItem.name + + override fun areContentsTheSame(oldItem: Repository, newItem: Repository): Boolean = + oldItem == newItem + } +} diff --git a/app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/fragment/RepositoryListFragment.kt b/app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/fragment/RepositoryListFragment.kt new file mode 100644 index 0000000..7ffed64 --- /dev/null +++ b/app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/fragment/RepositoryListFragment.kt @@ -0,0 +1,104 @@ +package fr.uca.iut.clfreville2.teaiswarm.fragment + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.viewModelScope +import androidx.paging.* +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import fr.uca.iut.clfreville2.teaiswarm.R +import fr.uca.iut.clfreville2.teaiswarm.adapter.RepositoryListAdapter +import fr.uca.iut.clfreville2.teaiswarm.model.Repository +import fr.uca.iut.clfreville2.teaiswarm.network.GiteaService +import fr.uca.iut.clfreville2.teaiswarm.network.RepositoryService +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import retrofit2.HttpException +import java.io.IOException + +class RepositoryListFragment(private val username: String, private val onClick: (Repository) -> Unit) : Fragment(R.layout.repository_list) { + + private val service = GiteaService() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + updateRepositories() + } + + private fun updateRepositories() { + val viewModel by viewModels( + factoryProducer = { + RepositoryViewModelFactory( + service, + username + ) + } + ) + val pagingAdapter = + RepositoryListAdapter(RepositoryListAdapter.RepositoryComparator, onClick) + val recyclerView = requireView().findViewById(R.id.repositories_view) + recyclerView.adapter = pagingAdapter + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + viewLifecycleOwner.lifecycleScope.launch { + viewModel.flow.collectLatest { pagingData -> + pagingAdapter.submitData(pagingData) + } + } + } + + class RepositorySource( + private val service: RepositoryService, + private val username: String + ) : PagingSource() { + + override suspend fun load(params: LoadParams): LoadResult = + try { + val nextPageNumber = params.key ?: 1 + val response = service.listActiveRepositories(username, nextPageNumber) + LoadResult.Page( + data = response, + prevKey = nextPageNumber - 1, + nextKey = nextPageNumber + 1 + ) + } catch (e: IOException) { + LoadResult.Error(e) + } catch (e: HttpException) { + LoadResult.Error(e) + } + + override fun getRefreshKey(state: PagingState): Int? = + state.anchorPosition?.let { anchorPosition -> + val anchorPage = state.closestPageToPosition(anchorPosition) + anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) + } + } + + class RepositoryViewModel( + private val service: RepositoryService, + private val username: String + ) : ViewModel() { + val flow = Pager( + PagingConfig(pageSize = 10, enablePlaceholders = true) + ) { + RepositorySource(service, username) + }.flow.cachedIn(viewModelScope) + } + + class RepositoryViewModelFactory( + private val service: RepositoryService, + private val username: String + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(RepositoryViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return RepositoryViewModel(service, username) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } + } +} diff --git a/app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/network/GiteaService.kt b/app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/network/GiteaService.kt index 8774022..2db0ded 100644 --- a/app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/network/GiteaService.kt +++ b/app/src/main/java/fr/uca/iut/clfreville2/teaiswarm/network/GiteaService.kt @@ -1,7 +1,6 @@ package fr.uca.iut.clfreville2.teaiswarm.network import com.squareup.moshi.Moshi -import com.squareup.moshi.adapters.EnumJsonAdapter import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import fr.uca.iut.clfreville2.teaiswarm.model.* @@ -33,7 +32,11 @@ class GiteaService(private val handle: GiteaApiService) : RepositoryService { constructor() : this(createRetrofit().create(GiteaApiService::class.java)) override suspend fun listActiveRepositories(username: String, page: Int): List = withContext(Dispatchers.IO) { - handle.listActiveRepositories(username, page) + if (page < 1) { + emptyList() + } else { + handle.listActiveRepositories(username, page) + } } override suspend fun listCommits( diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ebbeb24..93f23bc 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,32 +1,13 @@ - - - - -