Infinite scroll in the repository list
This commit is contained in:
@@ -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>
|
||||||
<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>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
9
app/src/main/res/layout/repository_list.xml
Normal file
9
app/src/main/res/layout/repository_list.xml
Normal file
@@ -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>
|
Reference in New Issue
Block a user