Skip to content

Commit 10fd6fb

Browse files
committed
feat(home): Auto-scroll to top on category switch
This commit introduces an automatic scroll-to-top feature on the home screen. When the user switches between categories (e.g., from "Trending" to "Hot Release"), the repository list now automatically scrolls to the beginning. - **feat(home)**: Implemented an event-based system (`HomeEvent`) to trigger UI actions from the `HomeViewModel`. - **feat(home)**: The list now scrolls to the top (`animateScrollToItem(0)`) after a new category's data is loaded. - **refactor(home)**: Modified the `loadRepos` function in `HomeViewModel` to return a `Job`, allowing the caller to wait for its completion before triggering subsequent actions like scrolling. - **refactor(home)**: Added a `trendingScore` property to the `GithubRepoSummary` domain model and the `CachedGithubRepoSummary` DTO, and updated the corresponding mapper.
1 parent c287617 commit 10fd6fb

7 files changed

Lines changed: 56 additions & 14 deletions

File tree

core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubRepoSummary.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ data class GithubRepoSummary(
1616
val language: String?,
1717
val topics: List<String>?,
1818
val releasesUrl: String,
19-
val updatedAt: String
19+
val updatedAt: String,
20+
val trendingScore: Double? = null
2021
)

feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/dto/CachedGithubRepoSummary.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,11 @@ data class CachedGithubRepoSummary(
1616
val language: String?,
1717
val topics: List<String>?,
1818
val releasesUrl: String,
19-
val updatedAt: String
19+
val updatedAt: String,
20+
val createdAt: String? = null,
21+
val latestReleaseDate: String? = null,
22+
val releaseRecency: Int? = null,
23+
val releaseRecencyText: String? = null,
24+
val trendingScore: Double? = null,
25+
val popularityScore: Int? = null
2026
)

feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/dto/CachedRepoResponse.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable
44

55
@Serializable
66
data class CachedRepoResponse(
7+
val category: String? = null,
78
val platform: String,
89
val lastUpdated: String,
910
val totalCount: Int,

feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/mappers/CachedGithubRepoSummaryMappers.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import zed.rainxch.core.domain.model.GithubRepoSummary
44
import zed.rainxch.core.domain.model.GithubUser
55
import zed.rainxch.home.data.dto.CachedGithubRepoSummary
66

7-
87
fun CachedGithubRepoSummary.toGithubRepoSummary(): GithubRepoSummary {
98
return GithubRepoSummary(
109
id = id,
@@ -24,6 +23,7 @@ fun CachedGithubRepoSummary.toGithubRepoSummary(): GithubRepoSummary {
2423
language = language,
2524
topics = topics,
2625
releasesUrl = releasesUrl,
27-
updatedAt = updatedAt
26+
updatedAt = latestReleaseDate ?: updatedAt,
27+
trendingScore = trendingScore
2828
)
2929
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package zed.rainxch.home.presentation
2+
3+
sealed interface HomeEvent {
4+
data object OnScrollToListTop : HomeEvent
5+
}

feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import androidx.compose.runtime.LaunchedEffect
3434
import androidx.compose.runtime.derivedStateOf
3535
import androidx.compose.runtime.getValue
3636
import androidx.compose.runtime.remember
37+
import androidx.compose.runtime.rememberCoroutineScope
3738
import androidx.compose.runtime.rememberUpdatedState
3839
import androidx.compose.ui.Alignment
3940
import androidx.compose.ui.Modifier
@@ -48,6 +49,7 @@ import zed.rainxch.githubstore.core.presentation.res.*
4849
import io.github.fletchmckee.liquid.LiquidState
4950
import io.github.fletchmckee.liquid.liquefiable
5051
import io.github.fletchmckee.liquid.rememberLiquidState
52+
import kotlinx.coroutines.launch
5153
import org.jetbrains.compose.resources.painterResource
5254
import org.jetbrains.compose.resources.stringResource
5355
import org.jetbrains.compose.ui.tooling.preview.Preview
@@ -57,7 +59,7 @@ import zed.rainxch.core.presentation.components.GithubStoreButton
5759
import zed.rainxch.core.presentation.components.RepositoryCard
5860
import zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid
5961
import zed.rainxch.core.presentation.theme.GithubStoreTheme
60-
import zed.rainxch.githubstore.core.presentation.res.*
62+
import zed.rainxch.core.presentation.utils.ObserveAsEvents
6163
import zed.rainxch.home.presentation.components.HomeFilterChips
6264
import zed.rainxch.home.domain.model.HomeCategory
6365

@@ -71,6 +73,18 @@ fun HomeRoot(
7173
viewModel: HomeViewModel = koinViewModel()
7274
) {
7375
val state by viewModel.state.collectAsStateWithLifecycle()
76+
val listState = rememberLazyStaggeredGridState()
77+
val scope = rememberCoroutineScope()
78+
79+
ObserveAsEvents(viewModel.events) { event ->
80+
when (event) {
81+
HomeEvent.OnScrollToListTop -> {
82+
scope.launch {
83+
listState.animateScrollToItem(0)
84+
}
85+
}
86+
}
87+
}
7488

7589
HomeScreen(
7690
state = state,
@@ -100,7 +114,8 @@ fun HomeRoot(
100114
viewModel.onAction(action)
101115
}
102116
}
103-
}
117+
},
118+
listState = listState
104119
)
105120
}
106121

@@ -109,8 +124,8 @@ fun HomeRoot(
109124
fun HomeScreen(
110125
state: HomeState,
111126
onAction: (HomeAction) -> Unit,
127+
listState: LazyStaggeredGridState,
112128
) {
113-
val listState = rememberLazyStaggeredGridState()
114129
val liquidState = LocalBottomNavigationLiquid.current
115130

116131
val shouldLoadMore by remember {
@@ -371,7 +386,8 @@ private fun Preview() {
371386
) {
372387
HomeScreen(
373388
state = HomeState(),
374-
onAction = {}
389+
onAction = {},
390+
listState = rememberLazyStaggeredGridState()
375391
)
376392
}
377393
}

feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
55
import kotlinx.coroutines.CancellationException
66
import kotlinx.coroutines.Job
7+
import kotlinx.coroutines.channels.Channel
78
import kotlinx.coroutines.flow.MutableStateFlow
89
import kotlinx.coroutines.flow.SharingStarted
910
import kotlinx.coroutines.flow.first
1011
import kotlinx.coroutines.flow.onStart
12+
import kotlinx.coroutines.flow.receiveAsFlow
1113
import kotlinx.coroutines.flow.stateIn
1214
import kotlinx.coroutines.flow.update
1315
import kotlinx.coroutines.launch
@@ -58,6 +60,9 @@ class HomeViewModel(
5860
initialValue = HomeState()
5961
)
6062

63+
private val _events = Channel<HomeEvent>()
64+
val events = _events.receiveAsFlow()
65+
6166
private fun syncSystemState() {
6267
viewModelScope.launch {
6368
try {
@@ -97,10 +102,10 @@ class HomeViewModel(
97102
}
98103
}
99104

100-
private fun loadRepos(isInitial: Boolean = false, category: HomeCategory? = null) {
105+
private fun loadRepos(isInitial: Boolean = false, category: HomeCategory? = null): Job? {
101106
if (_state.value.isLoading || _state.value.isLoadingMore) {
102107
logger.debug("Already loading, skipping...")
103-
return
108+
return null
104109
}
105110

106111
currentJob?.cancel()
@@ -113,8 +118,7 @@ class HomeViewModel(
113118

114119
logger.debug("Loading repos: category=$targetCategory, page=$nextPageIndex, isInitial=$isInitial")
115120

116-
currentJob = viewModelScope.launch {
117-
121+
return viewModelScope.launch {
118122
_state.update {
119123
it.copy(
120124
isLoading = isInitial,
@@ -128,7 +132,10 @@ class HomeViewModel(
128132
try {
129133
val flow = when (targetCategory) {
130134
HomeCategory.TRENDING -> homeRepository.getTrendingRepositories(nextPageIndex)
131-
HomeCategory.HOT_RELEASE -> homeRepository.getHotReleaseRepositories(nextPageIndex)
135+
HomeCategory.HOT_RELEASE -> homeRepository.getHotReleaseRepositories(
136+
nextPageIndex
137+
)
138+
132139
HomeCategory.MOST_POPULAR -> homeRepository.getMostPopular(nextPageIndex)
133140
}
134141

@@ -201,6 +208,8 @@ class HomeViewModel(
201208
)
202209
}
203210
}
211+
}.also {
212+
currentJob = it
204213
}
205214
}
206215

@@ -230,7 +239,11 @@ class HomeViewModel(
230239
is HomeAction.SwitchCategory -> {
231240
if (_state.value.currentCategory != action.category) {
232241
nextPageIndex = 1
233-
loadRepos(isInitial = true, category = action.category)
242+
viewModelScope.launch {
243+
loadRepos(isInitial = true, category = action.category)?.join()
244+
245+
_events.send(HomeEvent.OnScrollToListTop)
246+
}
234247
}
235248
}
236249

0 commit comments

Comments
 (0)