Skip to content

Commit 0ee2009

Browse files
authored
Merge pull request #14 from Javernaut/refactoring/one-shot-events
Refactoring of one-shot events
2 parents e4971a5 + c111dba commit 0ee2009

File tree

14 files changed

+274
-82
lines changed

14 files changed

+274
-82
lines changed

app/build.gradle.kts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,6 @@ dependencies {
143143
implementation(libs.androidx.navigation.compose)
144144

145145
implementation(libs.mediafile)
146-
147-
// TODO replace with own implementation
148-
implementation(libs.liveevent)
149146
}
150147

151148
detekt {

app/src/main/java/com/javernaut/whatthecodec/compose/playground/ComposePlaygroundActivity.kt

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,29 @@ import android.os.Bundle
44
import androidx.activity.compose.setContent
55
import androidx.activity.enableEdgeToEdge
66
import androidx.appcompat.app.AppCompatActivity
7-
import androidx.navigation.compose.NavHost
8-
import androidx.navigation.compose.composable
9-
import androidx.navigation.compose.rememberNavController
7+
import androidx.compose.foundation.layout.Box
8+
import androidx.compose.foundation.layout.fillMaxSize
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.material3.Button
11+
import androidx.compose.material3.Scaffold
12+
import androidx.compose.material3.SnackbarHost
13+
import androidx.compose.material3.SnackbarHostState
14+
import androidx.compose.material3.Text
15+
import androidx.compose.runtime.Composable
16+
import androidx.compose.runtime.remember
17+
import androidx.compose.runtime.rememberCoroutineScope
18+
import androidx.compose.ui.Alignment
19+
import androidx.compose.ui.Modifier
20+
import androidx.compose.ui.platform.LocalContext
21+
import androidx.lifecycle.ViewModel
22+
import androidx.lifecycle.viewModelScope
23+
import androidx.lifecycle.viewmodel.compose.viewModel
24+
import com.javernaut.whatthecodec.R
1025
import com.javernaut.whatthecodec.compose.theme.WhatTheCodecTheme
11-
import com.javernaut.whatthecodec.home.ui.screen.EmptyHomeScreen
12-
import com.javernaut.whatthecodec.settings.ui.SettingsScreen
26+
import com.javernaut.whatthecodec.home.ui.screen.ObserveAsEvents
27+
import kotlinx.coroutines.channels.Channel
28+
import kotlinx.coroutines.flow.receiveAsFlow
29+
import kotlinx.coroutines.launch
1330

1431
class ComposePlaygroundActivity : AppCompatActivity() {
1532

@@ -19,24 +36,58 @@ class ComposePlaygroundActivity : AppCompatActivity() {
1936

2037
setContent {
2138
WhatTheCodecTheme {
22-
val navController = rememberNavController()
23-
NavHost(navController = navController, startDestination = "home") {
24-
composable("home") {
25-
EmptyHomeScreen(
26-
onVideoIconClick = {},
27-
onAudioIconClick = {},
28-
onSettingsClicked = {
29-
navController.navigate("settings")
30-
}
31-
)
32-
}
33-
composable("settings") {
34-
SettingsScreen {
35-
navController.popBackStack()
36-
}
37-
}
39+
PlaygroundScreen()
40+
}
41+
}
42+
}
43+
}
44+
45+
class PlaygroundViewModel : ViewModel() {
46+
47+
private val _screenMessageChannel = Channel<PlaygroundScreenMessage>()
48+
val screenMessage = _screenMessageChannel.receiveAsFlow()
49+
50+
fun doSomething() {
51+
viewModelScope.launch {
52+
_screenMessageChannel.send(PlaygroundScreenMessage.FileOpeningError)
53+
}
54+
}
55+
}
56+
57+
sealed interface PlaygroundScreenMessage {
58+
data object FileOpeningError : PlaygroundScreenMessage
59+
}
60+
61+
@Composable
62+
private fun PlaygroundScreen(viewModel: PlaygroundViewModel = viewModel()) {
63+
val snackbarHostState = remember { SnackbarHostState() }
64+
val scope = rememberCoroutineScope()
65+
66+
val context = LocalContext.current
67+
ObserveAsEvents(flow = viewModel.screenMessage) {
68+
when (it) {
69+
PlaygroundScreenMessage.FileOpeningError -> {
70+
scope.launch {
71+
snackbarHostState.showSnackbar(context.getString(R.string.message_couldnt_open_file))
3872
}
3973
}
4074
}
4175
}
76+
77+
Scaffold(
78+
snackbarHost = {
79+
SnackbarHost(hostState = snackbarHostState)
80+
}
81+
) {
82+
Box(
83+
modifier = Modifier
84+
.fillMaxSize()
85+
.padding(it),
86+
contentAlignment = Alignment.Center
87+
) {
88+
Button(onClick = viewModel::doSomething) {
89+
Text(text = "Do something")
90+
}
91+
}
92+
}
4293
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.javernaut.whatthecodec.home.presentation
2+
3+
import android.content.ClipData
4+
import android.content.ClipboardManager
5+
import android.content.Context
6+
import androidx.core.content.getSystemService
7+
import dagger.hilt.android.qualifiers.ApplicationContext
8+
import javax.inject.Inject
9+
import javax.inject.Singleton
10+
11+
@Singleton
12+
class Clipboard @Inject constructor(
13+
@ApplicationContext context: Context
14+
) {
15+
private val clipboardManager = context.getSystemService<ClipboardManager>()!!
16+
17+
fun copy(value: String) {
18+
clipboardManager.setPrimaryClip(
19+
ClipData.newPlainText(null, value)
20+
)
21+
}
22+
}

app/src/main/java/com/javernaut/whatthecodec/home/presentation/MediaFileViewModel.kt

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import androidx.lifecycle.MutableLiveData
55
import androidx.lifecycle.SavedStateHandle
66
import androidx.lifecycle.ViewModel
77
import androidx.lifecycle.viewModelScope
8-
import com.hadilq.liveevent.LiveEvent
98
import com.javernaut.whatthecodec.home.presentation.model.AvailableTab
109
import com.javernaut.whatthecodec.home.presentation.model.BasicVideoInfo
1110
import com.javernaut.whatthecodec.home.presentation.model.FrameMetrics
@@ -16,10 +15,14 @@ import dagger.hilt.android.lifecycle.HiltViewModel
1615
import io.github.javernaut.mediafile.AudioStream
1716
import io.github.javernaut.mediafile.MediaFile
1817
import io.github.javernaut.mediafile.SubtitleStream
18+
import kotlinx.coroutines.channels.Channel
19+
import kotlinx.coroutines.flow.receiveAsFlow
20+
import kotlinx.coroutines.launch
1921
import javax.inject.Inject
2022

2123
@HiltViewModel
2224
class MediaFileViewModel @Inject constructor(
25+
private val clipboard: Clipboard,
2326
private val frameMetricsProvider: FrameMetricsProvider,
2427
private val mediaFileProvider: MediaFileProvider,
2528
private val savedStateHandle: SavedStateHandle
@@ -31,7 +34,7 @@ class MediaFileViewModel @Inject constructor(
3134
private var frameLoaderHelper: FrameLoaderHelper? = null
3235

3336
private val _screenState = MutableLiveData<ScreenState?>()
34-
private val _errorMessageLiveEvent = LiveEvent<Boolean>()
37+
private val _screenMessageChannel = Channel<ScreenMessage>()
3538

3639
init {
3740
pendingMediaFileArgument = savedStateHandle[KEY_MEDIA_FILE_ARGUMENT]
@@ -44,10 +47,9 @@ class MediaFileViewModel @Inject constructor(
4447
get() = _screenState
4548

4649
/**
47-
* Notifies about error during opening a file.
50+
* One time notifications about important events.
4851
*/
49-
val errorMessageLiveEvent: LiveData<Boolean>
50-
get() = _errorMessageLiveEvent
52+
val screenMessage = _screenMessageChannel.receiveAsFlow()
5153

5254
override fun onCleared() {
5355
if (frameLoaderHelper == null) {
@@ -75,7 +77,22 @@ class MediaFileViewModel @Inject constructor(
7577
mediaFile = newMediaFile
7678
applyMediaFile(newMediaFile)
7779
} else {
78-
_errorMessageLiveEvent.value = true
80+
sendMessage(ScreenMessage.FileOpeningError)
81+
}
82+
}
83+
84+
fun copyToClipboard(value: String) {
85+
clipboard.copy(value)
86+
sendMessage(ScreenMessage.ValueCopied(value))
87+
}
88+
89+
fun onPermissionDenied() {
90+
sendMessage(ScreenMessage.PermissionDeniedError)
91+
}
92+
93+
private fun sendMessage(message: ScreenMessage) {
94+
viewModelScope.launch {
95+
_screenMessageChannel.send(message)
7996
}
8097
}
8198

@@ -170,3 +187,9 @@ data class ScreenState(
170187
}
171188
}
172189
}
190+
191+
sealed interface ScreenMessage {
192+
data object FileOpeningError : ScreenMessage
193+
data object PermissionDeniedError : ScreenMessage
194+
class ValueCopied(val value: String) : ScreenMessage
195+
}

app/src/main/java/com/javernaut/whatthecodec/home/ui/RootActivity.kt

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@ package com.javernaut.whatthecodec.home.ui
33
import android.content.Intent
44
import android.net.Uri
55
import android.os.Bundle
6-
import android.widget.Toast
76
import androidx.activity.compose.setContent
87
import androidx.activity.enableEdgeToEdge
98
import androidx.activity.viewModels
109
import androidx.appcompat.app.AppCompatActivity
1110
import androidx.core.content.MimeTypeFilter
12-
import com.javernaut.whatthecodec.R
1311
import com.javernaut.whatthecodec.compose.theme.WhatTheCodecTheme
1412
import com.javernaut.whatthecodec.home.presentation.MediaFileArgument
1513
import com.javernaut.whatthecodec.home.presentation.MediaFileViewModel
@@ -41,12 +39,6 @@ class RootActivity : AppCompatActivity() {
4139
}
4240
}
4341

44-
mediaFileViewModel.errorMessageLiveEvent.observe(this) {
45-
if (it) {
46-
toast(R.string.message_couldnt_open_file)
47-
}
48-
}
49-
5042
intentActionViewConsumed =
5143
savedInstanceState?.getBoolean(EXTRA_INTENT_ACTION_VIEW_CONSUMED) == true
5244
if (!intentActionViewConsumed) {
@@ -104,7 +96,7 @@ class RootActivity : AppCompatActivity() {
10496
else -> actualPickAudioFile()
10597
}
10698
} else {
107-
toast(R.string.message_permission_denied)
99+
mediaFileViewModel.onPermissionDenied()
108100
}
109101
}
110102

@@ -179,10 +171,6 @@ class RootActivity : AppCompatActivity() {
179171
mediaFileViewModel.openMediaFile(MediaFileArgument(uri.toString(), mediaType))
180172
}
181173

182-
private fun toast(msg: Int) {
183-
Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
184-
}
185-
186174
companion object {
187175
private const val REQUEST_CODE_PICK_VIDEO = 41
188176
private const val REQUEST_CODE_PICK_AUDIO = 42

app/src/main/java/com/javernaut/whatthecodec/home/ui/audio/AudioPage.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,18 @@ import io.github.javernaut.mediafile.displayable.toDisplayable
2020
fun AudioPage(
2121
streams: List<AudioStream>,
2222
contentPadding: PaddingValues,
23+
onCopyValue: (String) -> Unit,
2324
modifier: Modifier = Modifier
2425
) {
2526
SimplePage(streams, contentPadding, modifier) { item, itemModifier ->
26-
AudioCardContent(item, itemModifier)
27+
AudioCardContent(item, onCopyValue, itemModifier)
2728
}
2829
}
2930

3031
@Composable
3132
private fun AudioCardContent(
3233
stream: AudioStream,
34+
onCopyValue: (String) -> Unit,
3335
modifier: Modifier = Modifier
3436
) {
3537
val streamFeatures = getFilteredStreamFeatures(
@@ -41,6 +43,7 @@ private fun AudioCardContent(
4143
StreamFeaturesGrid(
4244
stream,
4345
streamFeatures,
46+
onCopyValue,
4447
modifier
4548
)
4649
}

app/src/main/java/com/javernaut/whatthecodec/home/ui/screen/EmptyHomeScreen.kt

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,46 @@ import androidx.compose.material3.ExtendedFloatingActionButton
1313
import androidx.compose.material3.Icon
1414
import androidx.compose.material3.MaterialTheme
1515
import androidx.compose.material3.Scaffold
16+
import androidx.compose.material3.SnackbarHost
17+
import androidx.compose.material3.SnackbarHostState
1618
import androidx.compose.material3.Text
1719
import androidx.compose.material3.TopAppBar
1820
import androidx.compose.runtime.Composable
21+
import androidx.compose.runtime.remember
22+
import androidx.compose.runtime.rememberCoroutineScope
1923
import androidx.compose.ui.Alignment
2024
import androidx.compose.ui.Modifier
2125
import androidx.compose.ui.graphics.vector.ImageVector
26+
import androidx.compose.ui.platform.LocalContext
2227
import androidx.compose.ui.res.stringResource
2328
import androidx.compose.ui.text.style.TextAlign
2429
import androidx.compose.ui.tooling.preview.PreviewLightDark
2530
import androidx.compose.ui.unit.dp
2631
import com.javernaut.whatthecodec.R
2732
import com.javernaut.whatthecodec.compose.theme.WhatTheCodecTheme
33+
import com.javernaut.whatthecodec.home.presentation.ScreenMessage
34+
import kotlinx.coroutines.flow.Flow
35+
import kotlinx.coroutines.flow.emptyFlow
36+
import kotlinx.coroutines.launch
2837

2938
@Composable
3039
fun EmptyHomeScreen(
3140
onVideoIconClick: () -> Unit,
3241
onAudioIconClick: () -> Unit,
33-
onSettingsClicked: () -> Unit
42+
onSettingsClicked: () -> Unit,
43+
screenMassages: Flow<ScreenMessage>
3444
) {
45+
val snackbarHostState = remember { SnackbarHostState() }
46+
47+
ObserveScreenMessages(
48+
screenMassages = screenMassages,
49+
snackbarHostState = snackbarHostState
50+
)
51+
3552
Scaffold(
53+
snackbarHost = {
54+
SnackbarHost(hostState = snackbarHostState)
55+
},
3656
topBar = {
3757
EmptyScreenTopAppBar(onSettingsClicked)
3858
}
@@ -111,10 +131,38 @@ private fun EmptyScreenMainAction(
111131
)
112132
}
113133

134+
@Composable
135+
fun ObserveScreenMessages(
136+
screenMassages: Flow<ScreenMessage>,
137+
snackbarHostState: SnackbarHostState
138+
) {
139+
val scope = rememberCoroutineScope()
140+
val resources = LocalContext.current.resources
141+
142+
ObserveAsEvents(screenMassages) {
143+
scope.launch {
144+
snackbarHostState.showSnackbar(
145+
when (it) {
146+
ScreenMessage.FileOpeningError ->
147+
resources.getString(R.string.message_couldnt_open_file)
148+
149+
ScreenMessage.PermissionDeniedError ->
150+
resources.getString(R.string.message_permission_denied)
151+
152+
is ScreenMessage.ValueCopied ->
153+
resources.getString(
154+
R.string.stream_text_copied_pattern, it.value
155+
)
156+
}
157+
)
158+
}
159+
}
160+
}
161+
114162
@PreviewLightDark
115163
@Composable
116164
private fun EmptyScreenPreview() {
117165
WhatTheCodecTheme {
118-
EmptyHomeScreen({}, {}, {})
166+
EmptyHomeScreen({}, {}, {}, emptyFlow())
119167
}
120168
}

0 commit comments

Comments
 (0)