Skip to content

Commit 3c52c04

Browse files
committed
compose 1.5.5, polish sample
1 parent 5f4d3fc commit 3c52c04

File tree

9 files changed

+151
-25
lines changed

9 files changed

+151
-25
lines changed

.github/workflows/sample.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Build sample
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
paths-ignore: [ '**.md', '**.MD' ]
7+
tags-ignore:
8+
- '**'
9+
pull_request:
10+
branches: [ master ]
11+
paths-ignore: [ '**.md', '**.MD' ]
12+
workflow_dispatch:
13+
14+
env:
15+
GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
16+
17+
jobs:
18+
android:
19+
strategy:
20+
matrix:
21+
os: [ linux-latest ]
22+
runs-on: ${{ matrix.os }}
23+
steps:
24+
- uses: actions/checkout@v3
25+
26+
- name: Set up JDK
27+
uses: actions/setup-java@v3
28+
with:
29+
distribution: 'zulu'
30+
java-version: '17'
31+
32+
- name: Cache gradle, wrapper and buildSrc
33+
uses: actions/cache@v3
34+
with:
35+
path: |
36+
~/.gradle/caches
37+
~/.gradle/wrapper
38+
key: ${{ matrix.os }}-gradle-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/buildSrc/**/*.kt') }}
39+
restore-keys: |
40+
${{ matrix.os }}-gradle-
41+
- name: Cache konan
42+
uses: actions/cache@v3
43+
with:
44+
path: |
45+
~/.konan/cache
46+
~/.konan/dependencies
47+
~/.konan/kotlin-native-macos*
48+
~/.konan/kotlin-native-mingw*
49+
~/.konan/kotlin-native-windows*
50+
~/.konan/kotlin-native-linux*
51+
~/.konan/kotlin-native-prebuilt-macos*
52+
~/.konan/kotlin-native-prebuilt-mingw*
53+
~/.konan/kotlin-native-prebuilt-windows*
54+
~/.konan/kotlin-native-prebuilt-linux*
55+
key: ${{ matrix.os }}-konan-${{ hashFiles('**/*.gradle*') }}
56+
restore-keys: |
57+
${{ matrix.os }}-konan-
58+
- name: Make gradlew executable
59+
run: chmod +x ./gradlew
60+
61+
- name: Build
62+
run: ./gradlew :sample:app:assembleDebug --stacktrace

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ compose-rules-detekt = "0.3.5"
3636
androidx-lifecycle = "2.6.2"
3737
androidx-annotation = "1.7.0"
3838
androidx-activity = "1.8.1"
39-
androidx-compose-compiler = "1.5.5-dev-k1.9.21-163bb051fe5"
39+
androidx-compose-compiler = "1.5.5"
4040
androidx-navigation = "2.7.5"
4141
timber = "5.0.1"
4242

sample/app/src/main/java/com/hoc081098/channeleventbus/sample/android/common/CollectWithLifecycleEffect.kt

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.hoc081098.channeleventbus.sample.android.common
22

33
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.LaunchedEffect
45
import androidx.compose.runtime.NonRestartableComposable
56
import androidx.compose.runtime.RememberObserver
67
import androidx.compose.runtime.getValue
@@ -18,24 +19,41 @@ import kotlinx.coroutines.flow.Flow
1819
import kotlinx.coroutines.launch
1920

2021
/**
21-
* Collect the given [Flow] in a Effect that runs in the [Dispatchers.Main.immediate] coroutine,
22-
* when [LifecycleOwner.lifecycle] is at least at [minActiveState].
22+
* Collect the given [Flow] in an effect that runs when [LifecycleOwner.lifecycle] is at least at [minActiveState].
23+
*
24+
* If [inImmediateMain] is `true`, the effect will run in [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate],
25+
* otherwise it will run in [androidx.compose.runtime.Composer.applyCoroutineContext].
26+
*
27+
* @param keys Keys to be used to [remember] the effect.
28+
* @param lifecycleOwner The [LifecycleOwner] to be used to [repeatOnLifecycle].
29+
* @param minActiveState The minimum [Lifecycle.State] to be used to [repeatOnLifecycle].
30+
* @param inImmediateMain Whether the effect should run in [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
31+
* @param collector The collector to be used to collect the [Flow].
32+
*
33+
* @see [LaunchedEffect]
2334
*/
2435
@Composable
2536
fun <T> Flow<T>.CollectWithLifecycleEffect(
2637
vararg keys: Any?,
2738
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
2839
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
40+
inImmediateMain: Boolean = true,
2941
collector: (T) -> Unit,
3042
) {
3143
val flow = this
3244
val currentCollector by rememberUpdatedState(collector)
3345

34-
LaunchedEffectInImmediateMain(flow, lifecycleOwner, minActiveState, *keys) {
46+
val block: suspend CoroutineScope.() -> Unit = {
3547
lifecycleOwner.repeatOnLifecycle(minActiveState) {
36-
flow.collect { currentCollector(it) }
48+
flow.collect(currentCollector)
3749
}
3850
}
51+
52+
if (inImmediateMain) {
53+
LaunchedEffectInImmediateMain(flow, lifecycleOwner, minActiveState, *keys, block = block)
54+
} else {
55+
LaunchedEffect(flow, lifecycleOwner, minActiveState, *keys, block = block)
56+
}
3957
}
4058

4159
@Composable

sample/app/src/main/java/com/hoc081098/channeleventbus/sample/android/common/SingleEventChannel.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ interface HasSingleEventFlow<E> {
2020
/**
2121
* Must collect in [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
2222
* Safe to call in the coroutines launched by [androidx.lifecycle.lifecycleScope].
23+
*
24+
* In Compose, we can use [CollectWithLifecycleEffect] with `inImmediateMain = true`.
2325
*/
2426
val singleEventFlow: Flow<E>
2527
}
@@ -34,7 +36,7 @@ sealed interface SingleEventFlowSender<E> {
3436
}
3537

3638
@MainThread
37-
class SingleEventChannel<E> constructor() :
39+
class SingleEventChannel<E> :
3840
Closeable,
3941
HasSingleEventFlow<E>,
4042
SingleEventFlowSender<E> {

sample/app/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/home/detail/DetailVM.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.hoc081098.channeleventbus.ChannelEventBus
66
import com.hoc081098.channeleventbus.sample.android.common.SafeSavedStateHandle
77
import com.hoc081098.channeleventbus.sample.android.common.SavedStateHandleKey
88
import com.hoc081098.channeleventbus.sample.android.ui.home.DetailResultToHomeEvent
9+
import com.hoc081098.channeleventbus.sample.android.utils.NonBlankString.Companion.toNonBlankString
910
import kotlinx.coroutines.flow.StateFlow
1011

1112
class DetailVM(
@@ -21,8 +22,10 @@ class DetailVM(
2122
}
2223

2324
internal fun sendResultToHome() {
24-
val value = textStateFlow.value.takeIf { it.isNotBlank() } ?: return
25-
channelEventBus.send(DetailResultToHomeEvent(value))
25+
textStateFlow.value
26+
.toNonBlankString()
27+
.map(::DetailResultToHomeEvent)
28+
.onSuccess(channelEventBus::send)
2629
}
2730

2831
private companion object {

sample/app/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/home/events.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ package com.hoc081098.channeleventbus.sample.android.ui.home
22

33
import com.hoc081098.channeleventbus.ChannelEvent
44
import com.hoc081098.channeleventbus.ChannelEventKey
5+
import com.hoc081098.channeleventbus.sample.android.utils.NonBlankString
56

6-
internal data class DetailResultToHomeEvent(val value: String) : ChannelEvent<DetailResultToHomeEvent> {
7+
internal data class DetailResultToHomeEvent(val value: NonBlankString) : ChannelEvent<DetailResultToHomeEvent> {
78
override val key get() = Key
89

910
companion object Key : ChannelEventKey<DetailResultToHomeEvent>(DetailResultToHomeEvent::class)

sample/app/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/home/home/HomeScreen.kt

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import androidx.compose.foundation.layout.Spacer
88
import androidx.compose.foundation.layout.fillMaxSize
99
import androidx.compose.foundation.layout.height
1010
import androidx.compose.foundation.lazy.LazyColumn
11-
import androidx.compose.foundation.lazy.itemsIndexed
11+
import androidx.compose.foundation.lazy.items
1212
import androidx.compose.material3.ElevatedButton
1313
import androidx.compose.material3.MaterialTheme
1414
import androidx.compose.material3.Text
@@ -58,24 +58,21 @@ fun HomeScreen(
5858
LazyColumn(
5959
modifier = Modifier.weight(1f),
6060
contentPadding = PaddingValues(all = 16.dp),
61+
verticalArrangement = Arrangement.spacedBy(8.dp),
6162
) {
62-
itemsIndexed(
63+
items(
6364
items = detailResults,
64-
key = { _, item -> item },
65-
contentType = { _, _ -> "DetailResult" },
66-
) { index, result ->
65+
key = { it.asString() },
66+
contentType = { "DetailResult" },
67+
) { result ->
6768
Text(
6869
modifier = Modifier.fillParentMaxWidth(),
69-
text = result,
70+
text = result.asString(),
7071
style = MaterialTheme.typography.bodyMedium,
7172
textAlign = TextAlign.Start,
7273
maxLines = 2,
7374
overflow = TextOverflow.Ellipsis,
7475
)
75-
76-
if (index != detailResults.lastIndex) {
77-
Spacer(modifier = Modifier.height(8.dp))
78-
}
7976
}
8077
}
8178
}

sample/app/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/home/home/HomeVM.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,34 @@ import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
55
import com.hoc081098.channeleventbus.ChannelEventBus
66
import com.hoc081098.channeleventbus.sample.android.ui.home.DetailResultToHomeEvent
7+
import com.hoc081098.channeleventbus.sample.android.utils.NonBlankString
78
import kotlinx.collections.immutable.PersistentList
89
import kotlinx.collections.immutable.persistentListOf
910
import kotlinx.collections.immutable.plus
1011
import kotlinx.coroutines.CancellationException
1112
import kotlinx.coroutines.flow.SharingStarted
1213
import kotlinx.coroutines.flow.StateFlow
13-
import kotlinx.coroutines.flow.mapNotNull
14+
import kotlinx.coroutines.flow.map
1415
import kotlinx.coroutines.flow.onCompletion
1516
import kotlinx.coroutines.flow.scan
1617
import kotlinx.coroutines.flow.stateIn
1718

1819
class HomeVM(
1920
channelEventBus: ChannelEventBus,
2021
) : ViewModel() {
21-
internal val detailResultsStateFlow: StateFlow<PersistentList<String>> = channelEventBus
22+
internal val detailResultsStateFlow: StateFlow<PersistentList<NonBlankString>> = channelEventBus
2223
.receiveAsFlow(DetailResultToHomeEvent)
2324
.onCompletion {
2425
check(it is CancellationException) { "Expected CancellationException but was $it" }
2526
// Close the bus when this ViewModel is cleared.
2627
channelEventBus.closeKey(DetailResultToHomeEvent)
2728
}
28-
.mapNotNull { it.value.takeIf(String::isNotBlank) }
29-
.scan(persistentListOf<String>()) { acc, e ->
30-
if (e in acc) {
29+
.map { it.value }
30+
.scan(persistentListOf<NonBlankString>()) { acc, value ->
31+
if (value in acc) {
3132
acc
3233
} else {
33-
acc + e
34+
acc + value
3435
}
3536
}
3637
.stateIn(
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.hoc081098.channeleventbus.sample.android.utils
2+
3+
/**
4+
* Representation of strings that have at least one character, excluding
5+
* whitespaces.
6+
*/
7+
@JvmInline
8+
value class NonBlankString private constructor(private val value: String) : Comparable<NonBlankString> {
9+
init {
10+
require(value.isNotBlank()) { NotBlankStringException.message }
11+
}
12+
13+
/**
14+
* Compares this string alphabetically with the [other] one for order.
15+
* Returns zero if this string equals the [other] one, a negative number if
16+
* it's less than the [other] one, or a positive number if it's greater than
17+
* the [other] one.
18+
*/
19+
override infix fun compareTo(other: NonBlankString): Int = value.compareTo(other.value)
20+
21+
/** Returns this string as a [String]. */
22+
override fun toString(): String = value
23+
24+
fun asString(): String = value
25+
26+
/** Returns the length of this string. */
27+
val length: Int get() = value.length
28+
29+
companion object {
30+
/**
31+
* Returns this string as an encapsulated [NonBlankString],
32+
* or returns an encapsulated [IllegalArgumentException] if this string is
33+
* [blank][String.isBlank].
34+
*/
35+
fun String.toNonBlankString(): Result<NonBlankString> =
36+
runCatching { NonBlankString(this) }
37+
}
38+
}
39+
40+
internal object NotBlankStringException : IllegalArgumentException() {
41+
override val message: String = "Given string shouldn't be blank."
42+
}

0 commit comments

Comments
 (0)