Skip to content

Commit 2f7fb31

Browse files
committed
update(deps) + refactor
1 parent 08a85cc commit 2f7fb31

File tree

5 files changed

+184
-47
lines changed

5 files changed

+184
-47
lines changed

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ dependencies {
4646
implementation 'androidx.core:core-ktx:1.3.0'
4747
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
4848
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha03'
49+
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
4950
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
5051
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
5152
implementation 'androidx.fragment:fragment-ktx:1.2.4'
Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.hoc.mergeadapter_sample
22

3+
import android.util.Log
4+
import kotlinx.coroutines.Dispatchers
35
import kotlinx.coroutines.delay
4-
import java.util.concurrent.atomic.AtomicBoolean
5-
import java.util.concurrent.atomic.AtomicInteger
6+
import kotlinx.coroutines.withContext
7+
import kotlin.random.Random
68

79
//region Models
810
data class User(
@@ -16,21 +18,34 @@ object ApiError : Throwable(message = "Api error")
1618

1719
//region Fake api calling
1820
suspend fun getUsers(start: Int, limit: Int): List<User> {
19-
delay(2_000)
21+
return withContext(Dispatchers.IO) {
22+
Log.d("###", "getUsers { start: $start, limit: $limit }")
23+
delay(2_000)
2024

21-
if (count.getAndIncrement() == 2 && throwError.compareAndSet(true, false)) {
22-
throw ApiError
23-
}
25+
val page = start / limit
26+
27+
// throws at page 2
28+
if (page == 2 && Random.nextBoolean()) {
29+
throw ApiError
30+
}
2431

25-
return List(limit) {
26-
User(
27-
uid = start + it,
28-
name = "Name ${start + it}",
29-
email = "email${start + it}@gmail.com",
30-
)
32+
// throws at page 0
33+
if (page == 0 && Random.nextBoolean()) {
34+
throw ApiError
35+
}
36+
37+
// returns empty list at page 4
38+
if (page == 4) {
39+
emptyList()
40+
} else {
41+
List(limit) {
42+
User(
43+
uid = start + it,
44+
name = "Name ${start + it}",
45+
email = "email${start + it}@gmail.com",
46+
)
47+
}
48+
}
3149
}
3250
}
33-
34-
private val count = AtomicInteger(0)
35-
private val throwError = AtomicBoolean(true)
3651
//endregion

app/src/main/java/com/hoc/mergeadapter_sample/MainActivity.kt

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.hoc.mergeadapter_sample
22

33
import android.os.Bundle
4+
import android.view.View
45
import androidx.activity.viewModels
56
import androidx.appcompat.app.AppCompatActivity
67
import androidx.lifecycle.Observer
@@ -15,19 +16,38 @@ class MainActivity : AppCompatActivity() {
1516
private val viewModel by viewModels<MainVM>(viewModelFactoryProducer)
1617

1718
private val userAdapter = UserAdapter()
18-
private val footerAdapter = FooterAdapter(this::onRetry)
19+
private val footerAdapter = FooterAdapter(::onRetry)
1920

2021
override fun onCreate(savedInstanceState: Bundle?) {
2122
super.onCreate(savedInstanceState)
2223
setContentView(binding.root)
2324

25+
val linearLayoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
26+
2427
binding.recyclerView.run {
2528
setHasFixedSize(true)
26-
val linearLayoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
2729
layoutManager = linearLayoutManager
2830
adapter = MergeAdapter(userAdapter, footerAdapter)
31+
}
32+
33+
// observe livedatas
34+
viewModel.firstPageStateLiveData.observe(this, Observer(::renderFirstPageState))
35+
viewModel.loadingStateLiveData.observe(this, Observer(footerAdapter::submitList))
36+
viewModel.userLiveData.observe(this, Observer(userAdapter::submitList))
37+
viewModel.isRefreshingLiveData.observe(this, Observer {
38+
binding.swipeRefreshLayout.run {
39+
if (it) {
40+
post { isRefreshing = true }
41+
} else {
42+
isRefreshing = false
43+
}
44+
}
45+
})
2946

30-
addOnScrollListener(object : RecyclerView.OnScrollListener() {
47+
// bind action
48+
binding.run {
49+
swipeRefreshLayout.setOnRefreshListener { viewModel.refresh() }
50+
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
3151
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
3252
if (dy > 0
3353
&& linearLayoutManager.findLastVisibleItemPosition() + VISIBLE_THRESHOLD >= linearLayoutManager.itemCount
@@ -36,10 +56,26 @@ class MainActivity : AppCompatActivity() {
3656
}
3757
}
3858
})
59+
retryButton.setOnClickListener { viewModel.retryNextPage() }
3960
}
61+
}
4062

41-
viewModel.loadingStateLiveData.observe(this, Observer(footerAdapter::submitList))
42-
viewModel.userLiveData.observe(this, Observer(userAdapter::submitList))
63+
private fun renderFirstPageState(state: PlaceholderState) = binding.run {
64+
when (state) {
65+
PlaceholderState.Idle -> {
66+
errorGroup.visibility = View.GONE
67+
progressBar.visibility = View.INVISIBLE
68+
}
69+
PlaceholderState.Loading -> {
70+
errorGroup.visibility = View.GONE
71+
progressBar.visibility = View.VISIBLE
72+
}
73+
is PlaceholderState.Failure -> {
74+
errorGroup.visibility = View.VISIBLE
75+
progressBar.visibility = View.INVISIBLE
76+
errorText.text = state.throwable.message
77+
}
78+
}
4379
}
4480

4581
private fun onRetry() = viewModel.retryNextPage()

app/src/main/java/com/hoc/mergeadapter_sample/MainVM.kt

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,29 @@ sealed class PlaceholderState {
1212
data class Failure(val throwable: Throwable) : PlaceholderState()
1313
}
1414

15-
class MainVM(private val getUsers: suspend (start: Int, limit: Int) -> List<User>) : ViewModel() {
15+
class MainVM(
16+
private val getUsers: suspend (start: Int, limit: Int) -> List<User>
17+
) : ViewModel() {
1618

17-
//region Private
19+
//region Private fields
1820
private val usersD by lazy(NONE) {
1921
MutableLiveData<List<User>>()
2022
.apply { value = emptyList() }
21-
.also { loadNextPage() /* load first page when first accessing*/ }
23+
.also { loadNextPage() }
2224
}
2325
private val loadingStateD = MutableLiveData<PlaceholderState>().apply { value = Idle }
2426
private val firstPageStateD = MutableLiveData<PlaceholderState>().apply { value = Idle }
27+
private val isRefreshingD = MutableLiveData<Boolean>().apply { value = false }
2528

2629
private var isFirstPage = true
30+
private var loadedAllPage = false
2731

2832
private val shouldLoadNextPage: Boolean
2933
get() = if (isFirstPage) {
3034
firstPageStateD.value!! == Idle
3135
} else {
3236
loadingStateD.value!! == Idle
33-
}
37+
} && !loadedAllPage
3438

3539
private val shouldRetryNextPage: Boolean
3640
get() = if (isFirstPage) {
@@ -45,61 +49,79 @@ class MainVM(private val getUsers: suspend (start: Int, limit: Int) -> List<User
4549

4650
val userLiveData: LiveData<List<User>> get() = usersD
4751

48-
val firstPageStateLiveData: LiveData<PlaceholderState> = firstPageStateD
52+
val firstPageStateLiveData: LiveData<PlaceholderState> get() = firstPageStateD
4953

5054
val loadingStateLiveData: LiveData<List<PlaceholderState>>
5155
get() = loadingStateD.map { if (it == Idle) emptyList() else listOf(it) }
5256

57+
val isRefreshingLiveData: LiveData<Boolean> get() = isRefreshingD
58+
5359
//endregion
5460

5561
//region Public methods
5662
@MainThread
5763
fun loadNextPage() {
5864
if (shouldLoadNextPage) {
59-
loadNextPageInternal()
65+
loadPageInternal()
6066
}
6167
}
6268

6369
@MainThread
6470
fun retryNextPage() {
6571
if (shouldRetryNextPage) {
66-
loadNextPageInternal()
72+
loadPageInternal()
6773
}
6874
}
75+
76+
@MainThread
77+
fun refresh() {
78+
loadPageInternal(refresh = true)
79+
}
6980
//endregion
7081

71-
private fun loadNextPageInternal() {
82+
//region Private methods
83+
private fun updateState(state: PlaceholderState) {
84+
if (isFirstPage) {
85+
firstPageStateD.value = state
86+
} else {
87+
loadingStateD.value = state
88+
}
89+
}
90+
91+
private fun loadPageInternal(refresh: Boolean = false) {
7292
viewModelScope.launch {
73-
if (isFirstPage) {
74-
firstPageStateD.value = Loading
93+
if (refresh) {
94+
isRefreshingD.value = true
7595
} else {
76-
loadingStateD.value = Loading
96+
updateState(Loading)
7797
}
7898

79-
val currentList = usersD.value!!
99+
val currentList = if (refresh) emptyList() else usersD.value!!
80100

81101
runCatching { getUsers(currentList.size, LIMIT) }
82102
.fold(
83103
onSuccess = {
84-
isFirstPage = currentList.isEmpty()
85-
usersD.value = currentList + it
86-
87-
if (isFirstPage) {
88-
firstPageStateD.value = Idle
104+
if (refresh) {
105+
isRefreshingD.value = false
89106
} else {
90-
loadingStateD.value = Idle
107+
updateState(Idle)
91108
}
109+
usersD.value = currentList + it
110+
111+
isFirstPage = false
112+
loadedAllPage = it.isEmpty()
92113
},
93114
onFailure = {
94-
if (isFirstPage) {
95-
firstPageStateD.value = Failure(it)
115+
if (refresh) {
116+
isRefreshingD.value = false
96117
} else {
97-
loadingStateD.value = Failure(it)
118+
updateState(Failure(it))
98119
}
99120
}
100121
)
101122
}
102123
}
124+
//endregion
103125

104126
private companion object {
105127
const val LIMIT = 20
Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,80 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:app="http://schemas.android.com/apk/res-auto"
34
xmlns:tools="http://schemas.android.com/tools"
45
android:layout_width="match_parent"
56
android:layout_height="match_parent"
67
android:background="@android:color/white"
7-
88
tools:context=".MainActivity">
99

10-
<androidx.recyclerview.widget.RecyclerView
11-
android:id="@+id/recycler_view"
10+
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
11+
android:id="@+id/swipe_refresh_layout"
1212
android:layout_width="match_parent"
13-
android:layout_height="match_parent"
14-
android:scrollbars="vertical"
15-
tools:listitem="@layout/item_user" />
13+
android:layout_height="match_parent">
14+
15+
<androidx.recyclerview.widget.RecyclerView
16+
android:id="@+id/recycler_view"
17+
android:layout_width="match_parent"
18+
android:layout_height="match_parent"
19+
android:scrollbars="vertical"
20+
tools:listitem="@layout/item_user" />
21+
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
22+
23+
<ProgressBar
24+
android:id="@+id/progressBar"
25+
android:layout_width="wrap_content"
26+
android:layout_height="wrap_content"
27+
app:layout_constraintBottom_toBottomOf="parent"
28+
app:layout_constraintEnd_toEndOf="parent"
29+
app:layout_constraintHorizontal_bias="0.5"
30+
app:layout_constraintStart_toStartOf="parent"
31+
app:layout_constraintTop_toTopOf="parent" />
32+
33+
<Button
34+
android:id="@+id/retryButton"
35+
android:layout_width="wrap_content"
36+
android:layout_height="wrap_content"
37+
android:layout_marginTop="24dp"
38+
android:backgroundTint="@color/colorPrimary"
39+
android:elevation="12dp"
40+
android:paddingStart="32dp"
41+
android:paddingTop="16dp"
42+
android:paddingEnd="32dp"
43+
android:paddingBottom="16dp"
44+
android:text="Retry"
45+
android:textColor="@android:color/white"
46+
android:textSize="16sp"
47+
app:layout_constraintBottom_toTopOf="@+id/errorText"
48+
app:layout_constraintEnd_toEndOf="parent"
49+
app:layout_constraintHorizontal_bias="0.5"
50+
app:layout_constraintStart_toStartOf="parent"
51+
app:layout_constraintTop_toTopOf="parent"
52+
app:layout_constraintVertical_chainStyle="packed" />
53+
54+
<TextView
55+
android:id="@+id/errorText"
56+
android:layout_width="0dp"
57+
android:layout_height="wrap_content"
58+
android:layout_marginStart="8dp"
59+
android:layout_marginTop="8dp"
60+
android:layout_marginEnd="8dp"
61+
android:maxLines="2"
62+
android:text="TextView"
63+
android:textAlignment="center"
64+
android:textColor="#424242"
65+
android:textSize="15sp"
66+
app:layout_constraintBottom_toBottomOf="parent"
67+
app:layout_constraintEnd_toEndOf="parent"
68+
app:layout_constraintHorizontal_bias="0.5"
69+
app:layout_constraintStart_toStartOf="parent"
70+
app:layout_constraintTop_toBottomOf="@+id/retryButton" />
71+
72+
<androidx.constraintlayout.widget.Group
73+
android:id="@+id/errorGroup"
74+
android:layout_width="wrap_content"
75+
android:layout_height="wrap_content"
76+
android:visibility="gone"
77+
app:constraint_referenced_ids="retryButton,errorText"
78+
tools:visibility="visible" />
1679

1780
</androidx.constraintlayout.widget.ConstraintLayout>

0 commit comments

Comments
 (0)