Infinite scroll in the repository list

main
Clément FRÉVILLE 2 years ago
parent 1dd386c3d6
commit 35462dfe00

@ -3,39 +3,26 @@ package fr.uca.iut.clfreville2.teaiswarm
import android.content.Intent import android.content.Intent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle import android.os.Bundle
import android.widget.Button import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.fragment.app.FragmentFactory
import androidx.recyclerview.widget.RecyclerView 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.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_OWNER = "repository_owner"
const val REPOSITORY_NAME = "repository_name" const val REPOSITORY_NAME = "repository_name"
class MainActivity : AppCompatActivity() { 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?) { override fun onCreate(savedInstanceState: Bundle?) {
supportFragmentManager.fragmentFactory = RepositoryListFragmentFactory { repo ->
adapterOnClick(repo)
}
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
repositories = findViewById(R.id.repositories_view) supportFragmentManager.commit {
previousButton = findViewById(R.id.previous_repository_list) setReorderingAllowed(true)
nextButton = findViewById(R.id.next_repository_list) replace(R.id.fragment_container_view, RepositoryListFragment::class.java, null)
updateList()
previousButton.setOnClickListener {
currentPage = max(currentPage - 1, 0)
updateList()
}
nextButton.setOnClickListener {
currentPage += 1
updateList()
} }
} }
@ -46,12 +33,12 @@ class MainActivity : AppCompatActivity() {
startActivity(intent) startActivity(intent)
} }
private fun updateList() { class RepositoryListFragmentFactory(private val onClick: (Repository) -> Unit) : FragmentFactory() {
lifecycleScope.launch { override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
val repos = service.listActiveRepositories("clement.freville2", currentPage) if (className == RepositoryListFragment::class.java.name) {
repositories.adapter = RepositoryListAdapter(repos) { repo -> return RepositoryListFragment("clement.freville2", onClick);
adapterOnClick(repo)
} }
return super.instantiate(classLoader, className)
} }
} }
} }

@ -1,14 +1,17 @@
package fr.uca.iut.clfreville2.teaiswarm package fr.uca.iut.clfreville2.teaiswarm.adapter
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import fr.uca.iut.clfreville2.teaiswarm.R
import fr.uca.iut.clfreville2.teaiswarm.model.Repository import fr.uca.iut.clfreville2.teaiswarm.model.Repository
class RepositoryListAdapter(private val dataSet: List<Repository>, private val onClick: (Repository) -> Unit) : class RepositoryListAdapter(diffCallback: DiffUtil.ItemCallback<Repository>, private val onClick: (Repository) -> Unit) :
RecyclerView.Adapter<RepositoryListAdapter.ViewHolder>() { PagingDataAdapter<Repository, RepositoryListAdapter.ViewHolder>(diffCallback) {
class ViewHolder(view: View, private val onClick: (Repository) -> Unit) : RecyclerView.ViewHolder(view) { class ViewHolder(view: View, private val onClick: (Repository) -> Unit) : RecyclerView.ViewHolder(view) {
private val repositoryNameView: TextView private val repositoryNameView: TextView
@ -72,8 +75,14 @@ class RepositoryListAdapter(private val dataSet: List<Repository>, private val o
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(dataSet[position]) holder.bind(getItem(position))
} }
override fun getItemCount() = dataSet.size object RepositoryComparator : DiffUtil.ItemCallback<Repository>() {
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
}
} }

@ -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<RepositoryViewModel>(
factoryProducer = {
RepositoryViewModelFactory(
service,
username
)
}
)
val pagingAdapter =
RepositoryListAdapter(RepositoryListAdapter.RepositoryComparator, onClick)
val recyclerView = requireView().findViewById<RecyclerView>(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<Int, Repository>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repository> =
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, Repository>): 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 <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(RepositoryViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return RepositoryViewModel(service, username) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
}

@ -1,7 +1,6 @@
package fr.uca.iut.clfreville2.teaiswarm.network package fr.uca.iut.clfreville2.teaiswarm.network
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.EnumJsonAdapter
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import fr.uca.iut.clfreville2.teaiswarm.model.* import fr.uca.iut.clfreville2.teaiswarm.model.*
@ -33,8 +32,12 @@ class GiteaService(private val handle: GiteaApiService) : RepositoryService {
constructor() : this(createRetrofit().create(GiteaApiService::class.java)) constructor() : this(createRetrofit().create(GiteaApiService::class.java))
override suspend fun listActiveRepositories(username: String, page: Int): List<Repository> = withContext(Dispatchers.IO) { override suspend fun listActiveRepositories(username: String, page: Int): List<Repository> = withContext(Dispatchers.IO) {
if (page < 1) {
emptyList()
} else {
handle.listActiveRepositories(username, page) handle.listActiveRepositories(username, page)
} }
}
override suspend fun listCommits( override suspend fun listCommits(
repository: RepositoryIdentifiable, repository: RepositoryIdentifiable,

@ -1,32 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".MainActivity"> tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/repositories_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager"/> android:id="@+id/fragment_container_view" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/previous_repository_list"
android:text="@string/previous" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/next_repository_list"
android:text="@string/next" />
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/repositories_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Loading…
Cancel
Save