├── LICENSE ├── README.md └── flow ├── ComposeFlow.kt ├── Example.kt ├── Flow.kt └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Danny Lin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # app-utils 2 | 3 | Select parts of my homegrown library for Android app development. 4 | 5 | ## Components 6 | 7 | - [flow](flow/): Simple lifecycle-aware (State)Flow utilities for Android apps ([example](flow/Example.kt)) 8 | - [µlog](https://github.com/kdrag0n/ulog): Simple, fast, efficient logging facade 9 | -------------------------------------------------------------------------------- /flow/ComposeFlow.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Simple lifecycle-aware (State)Flow utilities for Android apps 3 | * 4 | * Licensed under the MIT License (MIT) 5 | * 6 | * Copyright (c) 2022 Danny Lin 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | * SOFTWARE. 25 | */ 26 | 27 | package dev.kdrag0n.app.ui.compose 28 | 29 | import android.annotation.SuppressLint 30 | import androidx.compose.runtime.Composable 31 | import androidx.compose.runtime.State 32 | import androidx.compose.runtime.collectAsState 33 | import androidx.compose.runtime.remember 34 | import androidx.compose.ui.platform.LocalLifecycleOwner 35 | import androidx.lifecycle.Lifecycle 36 | import androidx.lifecycle.flowWithLifecycle 37 | import kotlinx.coroutines.flow.Flow 38 | import kotlinx.coroutines.flow.StateFlow 39 | import kotlin.coroutines.CoroutineContext 40 | import kotlin.coroutines.EmptyCoroutineContext 41 | 42 | @SuppressLint("StateFlowValueCalledInComposition") 43 | @Composable 44 | fun StateFlow.collectAsLifecycleState( 45 | context: CoroutineContext = EmptyCoroutineContext, 46 | ) = collectAsLifecycleState( 47 | context = context, 48 | initial = value, 49 | ) 50 | 51 | 52 | @Composable 53 | fun Flow.collectAsLifecycleState( 54 | context: CoroutineContext = EmptyCoroutineContext, 55 | initial: T, 56 | ): State { 57 | val lifecycleOwner = LocalLifecycleOwner.current 58 | val flow = remember(this, lifecycleOwner) { 59 | flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED) 60 | } 61 | 62 | return flow.collectAsState(initial, context) 63 | } 64 | -------------------------------------------------------------------------------- /flow/Example.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Simple lifecycle-aware (State)Flow utilities for Android apps 3 | * 4 | * Licensed under the MIT License (MIT) 5 | * 6 | * Copyright (c) 2022 Danny Lin 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | * SOFTWARE. 25 | */ 26 | 27 | class ExampleFragment : Fragment() { 28 | private val model: ExampleViewModel by viewModels() 29 | 30 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 31 | launchStarted { 32 | model.text.launchCollect(this) { 33 | binding.textView.text = it 34 | } 35 | 36 | // Only update (change) events, doesn't get called with initial value 37 | model.text.launchCollectUpdates(this) { 38 | logD { "Text updated: $it" } 39 | } 40 | } 41 | } 42 | } 43 | 44 | 45 | @Composable 46 | fun ExampleScreen(model: ExampleViewModel) { 47 | val text by model.text.collectAsLifecycleState() 48 | 49 | Text(text) 50 | } 51 | -------------------------------------------------------------------------------- /flow/Flow.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Simple lifecycle-aware (State)Flow utilities for Android apps 3 | * 4 | * Licensed under the MIT License (MIT) 5 | * 6 | * Copyright (c) 2022 Danny Lin 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | * SOFTWARE. 25 | */ 26 | 27 | package dev.kdrag0n.app.utils 28 | 29 | import androidx.fragment.app.Fragment 30 | import androidx.lifecycle.* 31 | import dev.kdrag0n.app.log.logD 32 | import kotlinx.coroutines.CoroutineScope 33 | import kotlinx.coroutines.FlowPreview 34 | import kotlinx.coroutines.flow.* 35 | import kotlinx.coroutines.launch 36 | import kotlinx.coroutines.sync.Mutex 37 | 38 | /** All flow collectors in a Fragment should be in this block */ 39 | fun Fragment.launchStarted(block: suspend CoroutineScope.() -> Unit) = 40 | viewLifecycleOwner.launchStarted(block) 41 | 42 | /** All flow collectors in an Activity should be in this block */ 43 | fun LifecycleOwner.launchStarted(block: suspend CoroutineScope.() -> Unit) { 44 | lifecycleScope.launch { 45 | repeatOnLifecycle(Lifecycle.State.STARTED, block) 46 | } 47 | } 48 | 49 | /** For StateFlows: only collect update/change events, not initial value */ 50 | suspend fun Flow.collectUpdates(collector: FlowCollector) = 51 | drop(1).collect(collector) 52 | 53 | /** 54 | * For use in launchStarted blocks: non-blocking collect 55 | * Always use this to avoid blocking mistakes, as all collectors share the launchStarted block 56 | */ 57 | fun Flow.launchCollect(scope: CoroutineScope, collector: FlowCollector) = 58 | scope.launch { 59 | collect(collector) 60 | } 61 | 62 | /** 63 | * For use in launchStarted blocks: non-blocking collect -- update events only 64 | * Always use this to avoid blocking mistakes, as all collectors share the launchStarted block 65 | */ 66 | fun Flow.launchCollectUpdates(scope: CoroutineScope, collector: FlowCollector) = 67 | scope.launch { 68 | collectUpdates(collector) 69 | } 70 | 71 | /** Simple event flow with no associated data */ 72 | @OptIn(FlowPreview::class) 73 | class EventFlow : AbstractFlow() { 74 | private val flow = MutableSharedFlow() 75 | 76 | override suspend fun collectSafely(collector: FlowCollector) = 77 | flow.collect(collector) 78 | 79 | suspend fun emit() { 80 | flow.emit(Unit) 81 | } 82 | 83 | fun emit(viewModel: ViewModel) { 84 | viewModel.viewModelScope.launch { 85 | flow.emit(Unit) 86 | } 87 | } 88 | 89 | fun emit(lifecycleOwner: LifecycleOwner) { 90 | lifecycleOwner.lifecycleScope.launch { 91 | flow.emit(Unit) 92 | } 93 | } 94 | 95 | fun tryEmit() { 96 | flow.tryEmit(Unit) 97 | } 98 | } 99 | 100 | /** Drop-in replacement for MutableStateFlow that logs changes */ 101 | class DebugMutableStateFlow( 102 | private val orig: MutableStateFlow 103 | ) : MutableStateFlow by orig { 104 | override var value: T 105 | get() = orig.value 106 | set(value) { 107 | logD(Throwable()) { "setValue: flow=$orig | value=$value" } 108 | orig.value = value 109 | } 110 | } 111 | 112 | /** 113 | * Like Flow.combine, but returns a new StateFlow 114 | * Intended for use in ViewModels or lower layers 115 | */ 116 | fun StateFlow.combineState( 117 | other: StateFlow, 118 | scope: CoroutineScope, 119 | sharingStarted: SharingStarted = SharingStarted.Lazily, 120 | transform: (T1, T2) -> R, 121 | ) = combine(other) { a, b -> transform(a, b) } 122 | .stateIn(scope, sharingStarted, transform(value, other.value)) 123 | 124 | /** 125 | * Like Flow.map, but returns a new StateFlow 126 | * Intended for use in ViewModels or lower layers 127 | */ 128 | fun StateFlow.mapState( 129 | scope: CoroutineScope, 130 | sharingStarted: SharingStarted = SharingStarted.Eagerly, 131 | transform: (T) -> R, 132 | ) = map { transform(it) } 133 | .stateIn(scope, sharingStarted, transform(value)) 134 | 135 | /** Like Mutex.withLock, but doesn't run block if the lock is already held */ 136 | inline fun Mutex.tryWithLock(block: () -> T): T? { 137 | if (tryLock()) { 138 | try { 139 | return block() 140 | } finally { 141 | unlock() 142 | } 143 | } 144 | 145 | return null 146 | } 147 | -------------------------------------------------------------------------------- /flow/README.md: -------------------------------------------------------------------------------- 1 | # flow 2 | 3 | Simple lifecycle-aware (State)Flow utilities. 4 | 5 | ## Example 6 | 7 | ```kotlin 8 | class ExampleFragment : Fragment() { 9 | private val model: ExampleViewModel by viewModels() 10 | 11 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 12 | launchStarted { 13 | model.text.launchCollect(this) { 14 | binding.textView.text = it 15 | } 16 | 17 | // Only update (change) events, doesn't get called with initial value 18 | model.text.launchCollectUpdates(this) { 19 | logD { "Text updated: $it" } 20 | } 21 | } 22 | } 23 | } 24 | 25 | 26 | @Composable 27 | fun ExampleScreen(model: ExampleViewModel) { 28 | val text by model.text.collectAsLifecycleState() 29 | 30 | Text(text) 31 | } 32 | ``` 33 | --------------------------------------------------------------------------------