Skip to content

Commit a6e750b

Browse files
committed
Paging 3 sample
1 parent b7e8105 commit a6e750b

36 files changed

+723
-38
lines changed

app/src/main/java/com/alish/boilerplate/di/NetworkModule.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.alish.boilerplate.di
22

33
import android.content.Context
4+
import com.alish.boilerplate.data.remote.apiservices.FooApiService
45
import com.alish.boilerplate.data.remote.apiservices.SignInApiService
5-
import com.alish.boilerplate.data.remote.apiservices.SignInApiServiceImpl
6+
import com.alish.boilerplate.data.remote.apiservices.mock.FooApiServiceImpl
7+
import com.alish.boilerplate.data.remote.apiservices.mock.SignInApiServiceImpl
68
import dagger.Module
79
import dagger.Provides
810
import dagger.hilt.InstallIn
@@ -21,4 +23,12 @@ object NetworkModule {
2123
): SignInApiService {
2224
return SignInApiServiceImpl(context)
2325
}
26+
27+
@Singleton
28+
@Provides
29+
fun provideFooApiService(
30+
@ApplicationContext context: Context
31+
): FooApiService {
32+
return FooApiServiceImpl(context)
33+
}
2434
}

app/src/main/java/com/alish/boilerplate/di/RepositoriesModule.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.alish.boilerplate.di
22

3+
import com.alish.boilerplate.data.repositories.FooRepositoryImpl
34
import com.alish.boilerplate.data.repositories.SignInRepositoryImpl
5+
import com.alish.boilerplate.domain.repositories.FooRepository
46
import com.alish.boilerplate.domain.repositories.SignInRepository
57
import dagger.Binds
68
import dagger.Module
@@ -15,4 +17,9 @@ abstract class RepositoriesModule {
1517
abstract fun bindSignInRepository(
1618
signInRepositoryImpl: SignInRepositoryImpl
1719
): SignInRepository
20+
21+
@Binds
22+
abstract fun bindFooRepository(
23+
fooRepositoryImpl: FooRepositoryImpl
24+
): FooRepository
1825
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.alish.boilerplate.presentation.base
2+
3+
import androidx.recyclerview.widget.DiffUtil
4+
5+
interface IBaseDiffModel<T> {
6+
val id: T
7+
override fun equals(other: Any?): Boolean
8+
}
9+
10+
class BaseDiffUtilItemCallback<T : IBaseDiffModel<S>, S> : DiffUtil.ItemCallback<T>() {
11+
12+
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
13+
return oldItem.id == newItem.id
14+
}
15+
16+
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
17+
return oldItem == newItem
18+
}
19+
}

app/src/main/java/com/alish/boilerplate/presentation/base/BaseFragment.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment
99
import androidx.lifecycle.Lifecycle
1010
import androidx.lifecycle.lifecycleScope
1111
import androidx.lifecycle.repeatOnLifecycle
12+
import androidx.paging.PagingData
1213
import androidx.viewbinding.ViewBinding
1314
import com.alish.boilerplate.domain.utils.NetworkError
1415
import com.alish.boilerplate.presentation.ui.state.UIState
@@ -83,6 +84,16 @@ abstract class BaseFragment<ViewModel : BaseViewModel, Binding : ViewBinding>(
8384
}
8485
}
8586

87+
/**
88+
* Collect [PagingData] with [collectFlowSafely]
89+
*/
90+
protected fun <T : Any> Flow<PagingData<T>>.collectPaging(
91+
lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,
92+
action: suspend (value: PagingData<T>) -> Unit
93+
) {
94+
collectFlowSafely(lifecycleState) { this.collectLatest { action(it) } }
95+
}
96+
8697
/**
8798
* Setup views visibility depending on [UIState] states.
8899
* @param isShowViewIfSuccess is responsible for displaying views depending on whether

app/src/main/java/com/alish/boilerplate/presentation/base/BaseViewModel.kt

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package com.alish.boilerplate.presentation.base
22

3-
import androidx.lifecycle.ViewModel
3+
import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
5+
import androidx.paging.PagingData
6+
import androidx.paging.cachedIn
7+
import androidx.paging.map
58
import com.alish.boilerplate.domain.utils.Either
69
import com.alish.boilerplate.domain.utils.NetworkError
710
import com.alish.boilerplate.presentation.ui.state.UIState
811
import kotlinx.coroutines.Dispatchers
912
import kotlinx.coroutines.flow.Flow
1013
import kotlinx.coroutines.flow.MutableStateFlow
14+
import kotlinx.coroutines.flow.map
1115
import kotlinx.coroutines.launch
1216

1317
abstract class BaseViewModel : ViewModel() {
@@ -18,6 +22,18 @@ abstract class BaseViewModel : ViewModel() {
1822
@Suppress("FunctionName")
1923
protected fun <T> MutableUIStateFlow() = MutableStateFlow<UIState<T>>(UIState.Idle())
2024

25+
/**
26+
* Reset [MutableUIStateFlow] to [UIState.Idle]
27+
*/
28+
fun <T> MutableStateFlow<UIState<T>>.reset() {
29+
value = UIState.Idle()
30+
}
31+
32+
/**
33+
* Collect network request
34+
*
35+
* @return [UIState] depending request result
36+
*/
2137
protected fun <T> Flow<Either<NetworkError, T>>.collectRequest(
2238
state: MutableStateFlow<UIState<T>>,
2339
) {
@@ -33,11 +49,12 @@ abstract class BaseViewModel : ViewModel() {
3349
}
3450

3551
/**
36-
* Collect network request and return [UIState] depending request result
52+
* Collect network request with mapping from domain to ui
53+
*
54+
* @return [UIState] depending request result
3755
*/
3856
protected fun <T, S> Flow<Either<NetworkError, T>>.collectRequest(
39-
state: MutableStateFlow<UIState<S>>,
40-
mappedData: (T) -> S
57+
state: MutableStateFlow<UIState<S>>, mappedData: (T) -> S
4158
) {
4259
viewModelScope.launch(Dispatchers.IO) {
4360
state.value = UIState.Loading()
@@ -49,4 +66,11 @@ abstract class BaseViewModel : ViewModel() {
4966
}
5067
}
5168
}
69+
70+
/**
71+
* Collect paging request
72+
*/
73+
protected fun <T : Any, S : Any> Flow<PagingData<T>>.collectPagingRequest(
74+
mappedData: (T) -> S
75+
) = map { it.map { data -> mappedData(data) } }.cachedIn(viewModelScope)
5276
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.alish.boilerplate.presentation.models.foo
2+
3+
import com.alish.boilerplate.domain.models.foo.Foo
4+
import com.alish.boilerplate.presentation.base.IBaseDiffModel
5+
6+
data class FooUI(
7+
override val id: Long,
8+
val bar: String
9+
) : IBaseDiffModel<Long>
10+
11+
fun Foo.toUI() = FooUI(
12+
id, bar
13+
)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.alish.boilerplate.presentation.ui.adapters
2+
3+
import android.view.LayoutInflater
4+
import android.view.ViewGroup
5+
import androidx.paging.PagingDataAdapter
6+
import androidx.recyclerview.widget.RecyclerView
7+
import com.alish.boilerplate.presentation.base.BaseDiffUtilItemCallback
8+
import com.alish.boilerplate.databinding.ItemFooBinding
9+
import com.alish.boilerplate.presentation.models.foo.FooUI
10+
11+
class FooPagingAdapter : PagingDataAdapter<FooUI, FooPagingAdapter.FooPagingViewHolder>(
12+
BaseDiffUtilItemCallback()
13+
) {
14+
15+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FooPagingViewHolder {
16+
return FooPagingViewHolder(
17+
ItemFooBinding.inflate(LayoutInflater.from(parent.context), parent, false)
18+
)
19+
}
20+
21+
override fun onBindViewHolder(holder: FooPagingViewHolder, position: Int) {
22+
getItem(position)?.let { holder.onBind(it) }
23+
}
24+
25+
inner class FooPagingViewHolder(private val binding: ItemFooBinding) : RecyclerView.ViewHolder(
26+
binding.root
27+
) {
28+
29+
fun onBind(item: FooUI) = with(binding) {
30+
textItemFoo.text = item.bar
31+
}
32+
}
33+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.alish.boilerplate.presentation.ui.adapters.paging
2+
3+
import android.view.ViewGroup
4+
import androidx.paging.LoadState
5+
import androidx.paging.LoadStateAdapter
6+
7+
class CommonLoadStateAdapter(
8+
private val retry: () -> Unit
9+
) : LoadStateAdapter<CommonLoadStateViewHolder>() {
10+
11+
override fun onCreateViewHolder(
12+
parent: ViewGroup, loadState: LoadState
13+
): CommonLoadStateViewHolder {
14+
return CommonLoadStateViewHolder.create(parent, retry)
15+
}
16+
17+
override fun onBindViewHolder(holder: CommonLoadStateViewHolder, loadState: LoadState) {
18+
holder.bind(loadState)
19+
}
20+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.alish.boilerplate.presentation.ui.adapters.paging
2+
3+
import android.view.LayoutInflater
4+
import android.view.ViewGroup
5+
import androidx.core.view.isVisible
6+
import androidx.paging.LoadState
7+
import androidx.recyclerview.widget.RecyclerView
8+
import com.alish.boilerplate.R
9+
import com.alish.boilerplate.databinding.LoadStateFooterViewItemBinding
10+
11+
class CommonLoadStateViewHolder(
12+
private val binding: LoadStateFooterViewItemBinding,
13+
retry: () -> Unit
14+
) : RecyclerView.ViewHolder(binding.root) {
15+
16+
init {
17+
binding.retryButton.setOnClickListener { retry.invoke() }
18+
}
19+
20+
fun bind(loadState: LoadState) = with(binding) {
21+
if (loadState is LoadState.Error) {
22+
errorMsg.text = loadState.error.localizedMessage
23+
}
24+
25+
progressBar.isVisible = loadState is LoadState.Loading
26+
retryButton.isVisible = loadState is LoadState.Error
27+
errorMsg.isVisible = loadState is LoadState.Error
28+
}
29+
30+
companion object {
31+
fun create(parent: ViewGroup, retry: () -> Unit): CommonLoadStateViewHolder {
32+
val view = LayoutInflater.from(parent.context)
33+
.inflate(R.layout.load_state_footer_view_item, parent, false)
34+
val binding = LoadStateFooterViewItemBinding.bind(view)
35+
return CommonLoadStateViewHolder(binding, retry)
36+
}
37+
}
38+
}

app/src/main/java/com/alish/boilerplate/presentation/ui/fragments/home/HomeFragment.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package com.alish.boilerplate.presentation.ui.fragments.home
22

3+
import androidx.core.view.isVisible
34
import androidx.fragment.app.viewModels
5+
import androidx.paging.LoadState
6+
import androidx.recyclerview.widget.LinearLayoutManager
47
import by.kirich1409.viewbindingdelegate.viewBinding
58
import com.alish.boilerplate.R
69
import com.alish.boilerplate.databinding.FragmentHomeBinding
710
import com.alish.boilerplate.presentation.base.BaseFragment
11+
import com.alish.boilerplate.presentation.ui.adapters.FooPagingAdapter
12+
import com.alish.boilerplate.presentation.ui.adapters.paging.CommonLoadStateAdapter
813
import dagger.hilt.android.AndroidEntryPoint
914

1015
@AndroidEntryPoint
@@ -14,4 +19,32 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(
1419

1520
override val viewModel: HomeViewModel by viewModels()
1621
override val binding by viewBinding(FragmentHomeBinding::bind)
22+
23+
private val fooAdapter = FooPagingAdapter()
24+
25+
override fun initialize() {
26+
setupFooRecycler()
27+
}
28+
29+
private fun setupFooRecycler() = with(binding) {
30+
recyclerHomeFoo.layoutManager = LinearLayoutManager(context)
31+
recyclerHomeFoo.adapter = fooAdapter.withLoadStateFooter(
32+
footer = CommonLoadStateAdapter { fooAdapter.retry() }
33+
)
34+
35+
fooAdapter.addLoadStateListener { loadStates ->
36+
recyclerHomeFoo.isVisible = loadStates.refresh is LoadState.NotLoading
37+
binding.loaderHome.isVisible = loadStates.refresh is LoadState.Loading
38+
}
39+
}
40+
41+
override fun setupRequests() {
42+
fetchFoo()
43+
}
44+
45+
private fun fetchFoo() {
46+
viewModel.fetchFoo().collectPaging {
47+
fooAdapter.submitData(it)
48+
}
49+
}
1750
}

0 commit comments

Comments
 (0)