Skip to content

Commit 313a8b3

Browse files
authored
Saved state handle, kotlin 1.8.21, compose compiler 1.4.7, coroutines 1.7.0 (#56)
* simplify viewmodel * savedStateHandle * savedStateHandle * fix sideeffects * fix sideeffects * update ios * fix sideeffects * kotlin 1.8.21 * readme * test * autocloseable
1 parent ce8335c commit 313a8b3

File tree

26 files changed

+392
-166
lines changed

26 files changed

+392
-166
lines changed

README.md

Lines changed: 72 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Github Repos Search - Kotlin Multiplatform Mobile using Jetpack Compose, SwiftUI
77
[![iOS Build CI](https://github.com/hoc081098/GithubSearchKMM/actions/workflows/ios-build.yml/badge.svg)](https://github.com/hoc081098/GithubSearchKMM/actions/workflows/ios-build.yml)
88
[![Validate Gradle Wrapper](https://github.com/hoc081098/GithubSearchKMM/actions/workflows/gradle-wrapper-validation.yml/badge.svg)](https://github.com/hoc081098/GithubSearchKMM/actions/workflows/gradle-wrapper-validation.yml)
99
[![API](https://img.shields.io/badge/API-23%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=23)
10-
[![Kotlin](https://img.shields.io/badge/kotlin-1.8.10-blue.svg?logo=kotlin)](http://kotlinlang.org)
10+
[![Kotlin](https://img.shields.io/badge/kotlin-1.8.21-blue.svg?logo=kotlin)](http://kotlinlang.org)
1111
[![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fhoc081098%2FGithubSearchKMM&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com)
1212
[![License: MIT](https://img.shields.io/badge/License-MIT-purple.svg)](https://opensource.org/licenses/MIT)
1313
[![codecov](https://codecov.io/gh/hoc081098/GithubSearchKMM/branch/master/graph/badge.svg?token=qzSAFkj09P)](https://codecov.io/gh/hoc081098/GithubSearchKMM)
@@ -101,44 +101,73 @@ Liked some of my work? Buy me a coffee (or more likely a beer)
101101

102102
```kotlin
103103
public sealed interface FlowReduxStore<Action, State> {
104-
public val coroutineScope: CoroutineScope
105-
104+
/**
105+
* The state of this store.
106+
*/
106107
public val stateFlow: StateFlow<State>
107108

108-
/** Get streams of actions.
109-
*
110-
* This [Flow] includes dispatched [Action]s (via [dispatch] function)
111-
* and [Action]s returned from [SideEffect]s.
109+
/**
110+
* @return false if cannot dispatch action (this store was closed).
111+
*/
112+
public fun dispatch(action: Action): Boolean
113+
114+
/**
115+
* Call this method to close this store.
116+
* A closed store will not accept any action anymore, thus state will not change anymore.
117+
* All [SideEffect]s will be cancelled.
112118
*/
113-
public val actionSharedFlow: SharedFlow<Action>
119+
public fun close()
114120

115121
/**
116-
* @return false if cannot dispatch action ([coroutineScope] was cancelled).
122+
* After calling [close] method, this function will return true.
123+
*
124+
* @return true if this store was closed.
117125
*/
118-
public fun dispatch(action: Action): Boolean
126+
public fun isClosed(): Boolean
119127
}
120128
```
121129

122130
### Multiplatform ViewModel
131+
123132
```kotlin
124133
open class GithubSearchViewModel(
125134
searchRepoItemsUseCase: SearchRepoItemsUseCase,
135+
private val savedStateHandle: SavedStateHandle,
126136
) : ViewModel() {
137+
private val effectsContainer = GithubSearchSideEffectsContainer(searchRepoItemsUseCase)
138+
127139
private val store = viewModelScope.createFlowReduxStore(
128140
initialState = GithubSearchState.initial(),
129-
sideEffects = GithubSearchSideEffects(
130-
searchRepoItemsUseCase = searchRepoItemsUseCase,
131-
).sideEffects,
132-
reducer = { state, action -> action.reduce(state) }
141+
sideEffects = effectsContainer.sideEffects,
142+
reducer = Reducer(flip(GithubSearchAction::reduce))
143+
.withLogger(githubSearchFlowReduxLogger())
133144
)
134-
private val eventChannel = store.actionSharedFlow
135-
.mapNotNull { it.toGithubSearchSingleEventOrNull() }
136-
.buffer(Channel.UNLIMITED)
137-
.produceIn(viewModelScope)
138-
139-
fun dispatch(action: GithubSearchAction) = store.dispatch(action)
140-
val stateFlow: StateFlow<GithubSearchState> by store::stateFlow
141-
val eventFlow: Flow<GithubSearchSingleEvent> get() = eventChannel.receiveAsFlow()
145+
146+
val termStateFlow: NonNullStateFlowWrapper<String> = savedStateHandle.getStateFlow(TERM_KEY, "").wrap()
147+
val stateFlow: NonNullStateFlowWrapper<GithubSearchState> = store.stateFlow.wrap()
148+
val eventFlow: NonNullFlowWrapper<GithubSearchSingleEvent> = effectsContainer.eventFlow.wrap()
149+
150+
init {
151+
store.dispatch(InitialSearchAction(termStateFlow.value))
152+
}
153+
154+
@MainThread
155+
fun dispatch(action: GithubSearchAction): Boolean {
156+
if (action is GithubSearchAction.Search) {
157+
savedStateHandle[TERM_KEY] = action.term
158+
}
159+
return store.dispatch(action)
160+
}
161+
162+
companion object {
163+
private const val TERM_KEY = "com.hoc081098.github_search_kmm.presentation.GithubSearchViewModel.term"
164+
165+
/**
166+
* Used by non-Android platforms.
167+
*/
168+
fun create(searchRepoItemsUseCase: SearchRepoItemsUseCase): GithubSearchViewModel =
169+
GithubSearchViewModel(searchRepoItemsUseCase, SavedStateHandle())
170+
}
142171
}
143172
```
144173

@@ -150,8 +179,10 @@ Extends `GithubSearchViewModel` to use `Dagger Constructor Injection`.
150179

151180
```kotlin
152181
@HiltViewModel
153-
class DaggerGithubSearchViewModel @Inject constructor(searchRepoItemsUseCase: SearchRepoItemsUseCase) :
154-
GithubSearchViewModel(searchRepoItemsUseCase)
182+
class DaggerGithubSearchViewModel @Inject constructor(
183+
searchRepoItemsUseCase: SearchRepoItemsUseCase,
184+
savedStateHandle: SavedStateHandle,
185+
) : GithubSearchViewModel(searchRepoItemsUseCase, savedStateHandle)
155186
```
156187

157188
#### iOS
@@ -169,6 +200,7 @@ class IOSGithubSearchViewModel: ObservableObject {
169200
private let vm: GithubSearchViewModel
170201

171202
@Published private(set) var state: GithubSearchState
203+
@Published private(set) var term: String = ""
172204
let eventPublisher: AnyPublisher<GithubSearchSingleEventKs, Never>
173205

174206
init(vm: GithubSearchViewModel) {
@@ -179,11 +211,18 @@ class IOSGithubSearchViewModel: ObservableObject {
179211
.map(GithubSearchSingleEventKs.init)
180212
.eraseToAnyPublisher()
181213

182-
self.state = vm.stateFlow.typedValue()
183-
vm.stateFlow.subscribeNonNullFlow(
214+
self.state = vm.stateFlow.value
215+
vm.stateFlow.subscribe(
184216
scope: vm.viewModelScope,
185217
onValue: { [weak self] in self?.state = $0 }
186218
)
219+
220+
self.vm
221+
.termStateFlow
222+
.asNonNullPublisher(NSString.self)
223+
.assertNoFailure()
224+
.map { $0 as String }
225+
.assign(to: &$term)
187226
}
188227

189228
@discardableResult
@@ -242,14 +281,14 @@ class IOSGithubSearchViewModel: ObservableObject {
242281
--------------------------------------------------------------------------------
243282
Language Files Lines Blank Comment Code
244283
--------------------------------------------------------------------------------
245-
Kotlin 96 7111 863 398 5850
284+
Kotlin 105 7647 936 439 6272
246285
JSON 7 3938 0 0 3938
247-
Swift 16 857 110 98 649
248-
Markdown 1 255 47 0 208
249-
Bourne Shell 2 245 28 110 107
250-
Batch 1 91 21 0 70
251-
XML 7 71 6 0 65
286+
Swift 16 903 118 102 683
287+
Markdown 1 294 54 0 240
288+
Bourne Shell 2 250 28 116 106
289+
Batch 1 92 21 0 71
290+
XML 6 69 6 0 63
252291
--------------------------------------------------------------------------------
253-
Total 130 12568 1075 606 10887
292+
Total 138 13193 1163 657 11373
254293
--------------------------------------------------------------------------------
255294
```

androidApp/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ dependencies {
113113
implementation(deps.compose.uiToolingPreview)
114114
implementation(deps.compose.uiUtil)
115115
implementation(deps.compose.runtime)
116+
117+
lintChecks(deps.slack.composeLint)
116118
}
117119

118120
fun Project.buildComposeMetricsParameters(): List<String> {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (C) 2023 Slack Technologies, LLC
2+
// SPDX-License-Identifier: Apache-2.0
3+
package com.hoc081098.github_search_kmm.android
4+
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.runtime.Stable
7+
import androidx.compose.runtime.remember
8+
import androidx.compose.runtime.rememberCoroutineScope
9+
import kotlinx.coroutines.CoroutineScope
10+
11+
/**
12+
* Returns a [StableCoroutineScope] around a [rememberCoroutineScope]. This is useful for event
13+
* callback lambdas that capture a local scope variable to launch new coroutines, as it allows them
14+
* to be stable.
15+
*/
16+
@Composable
17+
fun rememberStableCoroutineScope(): StableCoroutineScope {
18+
val scope = rememberCoroutineScope()
19+
return remember { StableCoroutineScope(scope) }
20+
}
21+
22+
/** @see rememberStableCoroutineScope */
23+
@Stable
24+
class StableCoroutineScope(scope: CoroutineScope) : CoroutineScope by scope

androidApp/src/main/java/com/hoc081098/github_search_kmm/android/ui/GithubRepoItemsList.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ internal fun GithubRepoItemsList(
3636
error: AppError?,
3737
hasReachedMax: Boolean,
3838
onRetry: () -> Unit,
39-
onLoadNextPage: () -> Unit
39+
onLoadNextPage: () -> Unit,
40+
modifier: Modifier = Modifier,
4041
) {
4142
val lazyListState = rememberLazyListState()
4243
val currentOnLoadNextPage by rememberUpdatedState(onLoadNextPage)
@@ -70,7 +71,7 @@ internal fun GithubRepoItemsList(
7071
val decimalFormat = remember { StableWrapper(DecimalFormat("#,###")) }
7172

7273
LazyColumn(
73-
modifier = Modifier
74+
modifier = modifier
7475
.padding(horizontal = 16.dp),
7576
verticalArrangement = Arrangement.spacedBy(16.dp),
7677
state = lazyListState

0 commit comments

Comments
 (0)