diff --git a/.editorconfig b/.editorconfig
index 03e7ebb..5784868 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -60,7 +60,6 @@ ij_kotlin_extends_list_wrap = normal
ij_kotlin_field_annotation_wrap = split_into_lines
ij_kotlin_finally_on_new_line = false
ij_kotlin_if_rparen_on_new_line = true
-ij_kotlin_import_nested_classes = true
ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
ij_kotlin_keep_blank_lines_before_right_brace = 0
diff --git a/README.md b/README.md
index 9d2b2ce..2d708b0 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,10 @@ Splash screen
+Exoplayer video playback
+
+
+
### Quick overview
| Feature | Short description |
diff --git a/app/src/main/java/com/featuremodule/template/ui/AppNavBar.kt b/app/src/main/java/com/featuremodule/template/ui/AppNavBar.kt
index 83ea8eb..7f5ef95 100644
--- a/app/src/main/java/com/featuremodule/template/ui/AppNavBar.kt
+++ b/app/src/main/java/com/featuremodule/template/ui/AppNavBar.kt
@@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
+import com.featuremodule.core.navigation.HIDE_NAV_BAR
import com.featuremodule.core.navigation.NavBarItems
/**
@@ -24,6 +25,8 @@ internal fun AppNavBar(
openNavBarRoute: (route: String, isSelected: Boolean) -> Unit,
currentDestination: NavDestination?,
) {
+ if (currentDestination?.route.orEmpty().contains(HIDE_NAV_BAR)) return
+
NavigationBar {
NavBarItems.entries.forEach { item ->
val isSelected = currentDestination?.hierarchy
diff --git a/core/src/main/java/com/featuremodule/core/navigation/NavUtils.kt b/core/src/main/java/com/featuremodule/core/navigation/NavUtils.kt
new file mode 100644
index 0000000..ec45cc2
--- /dev/null
+++ b/core/src/main/java/com/featuremodule/core/navigation/NavUtils.kt
@@ -0,0 +1,4 @@
+package com.featuremodule.core.navigation
+
+/** Appended to the start of a route to sign that NavigationBar should be hidden */
+const val HIDE_NAV_BAR = "hide_nav_bar/"
diff --git a/core/src/main/java/com/featuremodule/core/util/ContextUtils.kt b/core/src/main/java/com/featuremodule/core/util/ContextUtils.kt
new file mode 100644
index 0000000..3af26a8
--- /dev/null
+++ b/core/src/main/java/com/featuremodule/core/util/ContextUtils.kt
@@ -0,0 +1,11 @@
+package com.featuremodule.core.util
+
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
+
+tailrec fun Context.getActivity(): Activity? = when (this) {
+ is Activity -> this
+ is ContextWrapper -> baseContext.getActivity()
+ else -> null
+}
diff --git a/core/src/main/java/com/featuremodule/core/util/Utils.kt b/core/src/main/java/com/featuremodule/core/util/FlowUtils.kt
similarity index 100%
rename from core/src/main/java/com/featuremodule/core/util/Utils.kt
rename to core/src/main/java/com/featuremodule/core/util/FlowUtils.kt
diff --git a/detekt-config.yml b/detekt-config.yml
index 76740b4..ccbeec5 100644
--- a/detekt-config.yml
+++ b/detekt-config.yml
@@ -366,7 +366,7 @@ naming:
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
TopLevelPropertyNaming:
active: true
- constantPattern: '[A-Z][A-Za-z0-9]*'
+ constantPattern: '[A-Za-z][_A-Za-z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
VariableMaxLength:
diff --git a/feature/homeImpl/build.gradle.kts b/feature/homeImpl/build.gradle.kts
index b5d7a9e..37148a3 100644
--- a/feature/homeImpl/build.gradle.kts
+++ b/feature/homeImpl/build.gradle.kts
@@ -9,4 +9,6 @@ android {
dependencies {
implementation(projects.feature.homeApi)
implementation(projects.feature.featureAApi)
+
+ implementation(libs.bundles.exoplayer)
}
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt
index e39fdc9..a966086 100644
--- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt
@@ -2,11 +2,25 @@ package com.featuremodule.homeImpl
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
+import com.featuremodule.core.navigation.HIDE_NAV_BAR
import com.featuremodule.homeApi.HomeDestination
+import com.featuremodule.homeImpl.exoplayer.ExoplayerScreen
import com.featuremodule.homeImpl.ui.HomeScreen
fun NavGraphBuilder.registerHome() {
composable(HomeDestination.ROUTE) { backStackEntry ->
HomeScreen(route = backStackEntry.destination.route)
}
+
+ composable(InternalRoutes.ExoplayerDestination.ROUTE) {
+ ExoplayerScreen()
+ }
+}
+
+internal sealed class InternalRoutes {
+ object ExoplayerDestination {
+ const val ROUTE = HIDE_NAV_BAR + "exoplayer"
+
+ fun constructRoute() = ROUTE
+ }
}
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/exoplayer/ExoplayerContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/exoplayer/ExoplayerContract.kt
new file mode 100644
index 0000000..2ff9f3a
--- /dev/null
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/exoplayer/ExoplayerContract.kt
@@ -0,0 +1,22 @@
+package com.featuremodule.homeImpl.exoplayer
+
+import androidx.media3.exoplayer.ExoPlayer
+import com.featuremodule.core.ui.UiEvent
+import com.featuremodule.core.ui.UiState
+
+internal data class State(
+ val exoplayer: ExoPlayer,
+ val overlayState: OverlayState = OverlayState(),
+) : UiState
+
+internal data class OverlayState(
+ val showPlayButton: Boolean = false,
+ val title: String = "",
+ val contentPosition: Long = 0,
+ val contentDuration: Long = 0,
+)
+
+internal sealed interface Event : UiEvent {
+ data object OnPlayPauseClick : Event
+ data class OnSeekFinished(val position: Long) : Event
+}
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/exoplayer/ExoplayerModule.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/exoplayer/ExoplayerModule.kt
new file mode 100644
index 0000000..b6d1776
--- /dev/null
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/exoplayer/ExoplayerModule.kt
@@ -0,0 +1,18 @@
+package com.featuremodule.homeImpl.exoplayer
+
+import android.content.Context
+import androidx.media3.exoplayer.ExoPlayer
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.android.scopes.ViewModelScoped
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class ExoplayerModule {
+ @ViewModelScoped
+ @Provides
+ fun provideExoplayer(@ApplicationContext context: Context) = ExoPlayer.Builder(context).build()
+}
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/exoplayer/ExoplayerScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/exoplayer/ExoplayerScreen.kt
new file mode 100644
index 0000000..494560d
--- /dev/null
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/exoplayer/ExoplayerScreen.kt
@@ -0,0 +1,115 @@
+package com.featuremodule.homeImpl.exoplayer
+
+import androidx.annotation.OptIn
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.ui.AspectRatioFrameLayout
+import androidx.media3.ui.PlayerView
+import com.featuremodule.core.util.getActivity
+
+@Composable
+internal fun ExoplayerScreen(viewModel: ExoplayerVM = hiltViewModel()) {
+ val context = LocalContext.current
+ val state by viewModel.state.collectAsStateWithLifecycle()
+
+ // Hide system bars just for this dialog
+ DisposableEffect(Unit) {
+ val window = context.getActivity()?.window ?: return@DisposableEffect onDispose {}
+ val insetsController = WindowCompat.getInsetsController(window, window.decorView)
+
+ insetsController.apply {
+ hide(WindowInsetsCompat.Type.systemBars())
+ systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ }
+
+ onDispose {
+ insetsController.apply {
+ show(WindowInsetsCompat.Type.systemBars())
+ systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
+ }
+ }
+ }
+
+ ExoplayerScreen(
+ state = state,
+ postEvent = viewModel::postEvent,
+ )
+}
+
+@OptIn(UnstableApi::class)
+@Composable
+private fun ExoplayerScreen(state: State, postEvent: (Event) -> Unit) {
+ var overlayVisibility by rememberSaveable { mutableStateOf(true) }
+ var videoSize by remember { mutableStateOf(IntSize(0, 0)) }
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.DarkGray)
+ // Indication is not needed and it does not look good
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ ) {
+ overlayVisibility = !overlayVisibility
+ },
+ contentAlignment = Alignment.Center,
+ ) {
+ AndroidView(
+ factory = { viewContext ->
+ PlayerView(viewContext).apply {
+ useController = false
+ setShowBuffering(PlayerView.SHOW_BUFFERING_ALWAYS)
+ background = context.getDrawable(android.R.color.black)
+ resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
+
+ player = state.exoplayer
+ }
+ },
+ modifier = Modifier.onSizeChanged { intSize ->
+ videoSize = intSize
+ },
+ )
+
+ Overlay(
+ state = state.overlayState,
+ isVisible = overlayVisibility,
+ onPlayPauseClick = { postEvent(Event.OnPlayPauseClick) },
+ onSeek = { postEvent(Event.OnSeekFinished(it)) },
+ modifier = Modifier.size(
+ with(LocalDensity.current) {
+ DpSize(
+ width = videoSize.width.toDp(),
+ height = videoSize.height.toDp(),
+ )
+ },
+ ),
+ )
+ }
+}
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/exoplayer/ExoplayerVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/exoplayer/ExoplayerVM.kt
new file mode 100644
index 0000000..4ef0a87
--- /dev/null
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/exoplayer/ExoplayerVM.kt
@@ -0,0 +1,127 @@
+package com.featuremodule.homeImpl.exoplayer
+
+import android.content.ContentResolver
+import android.net.Uri
+import androidx.annotation.OptIn
+import androidx.media3.common.MediaItem
+import androidx.media3.common.Player
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.common.util.Util
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.exoplayer.SeekParameters
+import com.featuremodule.core.ui.BaseVM
+import com.featuremodule.homeImpl.R
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import javax.inject.Inject
+
+@OptIn(UnstableApi::class)
+@HiltViewModel
+internal class ExoplayerVM @Inject constructor(
+ private val exoplayer: ExoPlayer,
+) : BaseVM() {
+ private var progressUpdateJob: Job? = null
+
+ private val playerEventListener = object : Player.Listener {
+ override fun onEvents(player: Player, events: Player.Events) {
+ var updatedState = state.value.overlayState
+
+ if (events.containsAny(
+ Player.EVENT_PLAY_WHEN_READY_CHANGED,
+ Player.EVENT_PLAYBACK_STATE_CHANGED,
+ Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED,
+ )
+ ) {
+ updatedState = updatedState.copy(
+ showPlayButton = Util.shouldShowPlayButton(exoplayer),
+ )
+ }
+
+ if (events.contains(Player.EVENT_IS_PLAYING_CHANGED)) {
+ if (player.isPlaying) {
+ startProgressUpdateJob()
+ } else {
+ stopProgressUpdateJob()
+ updatedState = updatedState.copy(
+ contentPosition = player.currentPosition,
+ contentDuration = player.contentDuration.coerceAtLeast(0L),
+ )
+ }
+ }
+
+ setState { copy(overlayState = updatedState) }
+ }
+ }
+
+ init {
+ with(exoplayer) {
+ setMediaItem(
+ MediaItem.fromUri(
+ Uri.Builder()
+ .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+ .path(R.raw.fumo_sail.toString())
+ .build(),
+ ),
+ )
+ // Slow but exact, can be adjusted if needed
+ setSeekParameters(SeekParameters.EXACT)
+ prepare()
+ playWhenReady = true
+
+ addListener(playerEventListener)
+ }
+ }
+
+ @Suppress("MagicNumber")
+ @Synchronized
+ private fun startProgressUpdateJob() {
+ // if job is null, this will not trigger too
+ if (progressUpdateJob?.isActive == true) return
+
+ progressUpdateJob = launch {
+ while (isActive) {
+ setState {
+ val newOverlay = overlayState.copy(
+ contentPosition = exoplayer.currentPosition,
+ contentDuration = exoplayer.contentDuration.coerceAtLeast(0L),
+ )
+ copy(overlayState = newOverlay)
+ }
+ delay(1000L - exoplayer.currentPosition % 1000L)
+ }
+ }
+ }
+
+ @Synchronized
+ private fun stopProgressUpdateJob() {
+ progressUpdateJob?.cancel()
+ progressUpdateJob = null
+ }
+
+ override fun initialState() = State(
+ exoplayer = exoplayer,
+ overlayState = OverlayState(title = "fumo_sail"),
+ )
+
+ override fun handleEvent(event: Event) {
+ when (event) {
+ Event.OnPlayPauseClick -> {
+ Util.handlePlayPauseButtonAction(exoplayer)
+ }
+
+ is Event.OnSeekFinished -> onSeek(event.position)
+ }
+ }
+
+ private fun onSeek(position: Long) {
+ exoplayer.seekTo(position)
+ setState { copy(overlayState = overlayState.copy(contentPosition = position)) }
+ }
+
+ override fun onCleared() {
+ exoplayer.removeListener(playerEventListener)
+ exoplayer.release()
+ }
+}
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/exoplayer/Overlay.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/exoplayer/Overlay.kt
new file mode 100644
index 0000000..7de98a7
--- /dev/null
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/exoplayer/Overlay.kt
@@ -0,0 +1,156 @@
+package com.featuremodule.homeImpl.exoplayer
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.featuremodule.homeImpl.R
+import kotlin.time.Duration.Companion.milliseconds
+
+@Composable
+internal fun Overlay(
+ state: OverlayState,
+ isVisible: Boolean,
+ onPlayPauseClick: () -> Unit,
+ onSeek: (Long) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ AnimatedVisibility(
+ visible = isVisible,
+ enter = fadeIn(),
+ exit = fadeOut(),
+ ) {
+ Column(
+ modifier = modifier.background(Color.Black.copy(alpha = 0.5f)),
+ verticalArrangement = Arrangement.SpaceBetween,
+ ) {
+ TopPart(
+ title = state.title,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp),
+ )
+
+ CenterPart(
+ showPlayButton = state.showPlayButton,
+ onPlayPauseClick = onPlayPauseClick,
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ )
+
+ BottomPart(
+ contentPosition = state.contentPosition,
+ contentDuration = state.contentDuration,
+ onSeek = onSeek,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(40.dp)
+ .padding(top = 8.dp, start = 8.dp, end = 8.dp),
+ )
+ }
+ }
+}
+
+@Composable
+private fun TopPart(title: String, modifier: Modifier = Modifier) = Row(modifier = modifier) {
+ Text(
+ text = title,
+ color = Color.White,
+ fontWeight = FontWeight.SemiBold,
+ modifier = Modifier.padding(8.dp),
+ )
+}
+
+@Composable
+private fun CenterPart(
+ showPlayButton: Boolean,
+ onPlayPauseClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) = Row(modifier = modifier) {
+ IconButton(
+ onClick = onPlayPauseClick,
+ colors = IconButtonDefaults.iconButtonColors(
+ contentColor = Color.White,
+ ),
+ ) {
+ if (showPlayButton) {
+ Icon(painterResource(id = R.drawable.play), contentDescription = null)
+ } else {
+ Icon(painterResource(id = R.drawable.pause), contentDescription = null)
+ }
+ }
+}
+
+@Composable
+private fun BottomPart(
+ contentPosition: Long,
+ contentDuration: Long,
+ onSeek: (Long) -> Unit,
+ modifier: Modifier = Modifier,
+) = Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically,
+) {
+ var isValueChanging by remember { mutableStateOf(false) }
+ var seekPosition by remember { mutableFloatStateOf(0f) }
+
+ Text(
+ text = "${formatMs(contentPosition)}/${formatMs(contentDuration)}",
+ color = Color.White,
+ fontSize = 12.sp,
+ modifier = Modifier.padding(end = 8.dp),
+ )
+
+ Slider(
+ value = if (isValueChanging) {
+ seekPosition
+ } else {
+ contentPosition.toFloat()
+ },
+ onValueChange = {
+ seekPosition = it
+ isValueChanging = true
+ },
+ valueRange = 0f..contentDuration.toFloat(),
+ modifier = Modifier.fillMaxWidth(),
+ onValueChangeFinished = {
+ onSeek(seekPosition.toLong())
+ isValueChanging = false
+ },
+ )
+}
+
+private fun formatMs(ms: Long): String {
+ return ms.milliseconds.toComponents { hours, minutes, seconds, _ ->
+ val secondsString = "%02d".format(seconds)
+ if (hours == 0L) {
+ "$minutes:$secondsString"
+ } else {
+ val minutesString = "%02d".format(minutes)
+ "$hours:$minutesString:$secondsString"
+ }
+ }
+}
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt
index d9e0b1e..e5bc0c6 100644
--- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt
@@ -7,4 +7,5 @@ internal data object State : UiState
internal sealed interface Event : UiEvent {
data object NavigateToFeatureA : Event
+ data object NavigateToExoplayer : Event
}
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt
index 672979b..2b7a3e5 100644
--- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt
@@ -1,17 +1,45 @@
package com.featuremodule.homeImpl.ui
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@Composable
internal fun HomeScreen(route: String?, viewModel: HomeVM = hiltViewModel()) {
- Column {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
Text(text = route.toString())
- Button(onClick = { viewModel.postEvent(Event.NavigateToFeatureA) }) {
- Text(text = "Pass number")
+
+ Column(
+ modifier = Modifier.width(IntrinsicSize.Max),
+ ) {
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { viewModel.postEvent(Event.NavigateToFeatureA) },
+ ) {
+ Text(text = "Pass number")
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { viewModel.postEvent(Event.NavigateToExoplayer) },
+ ) {
+ Text(text = "Exoplayer")
+ }
}
}
}
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt
index 2cfa27a..e274dd8 100644
--- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt
@@ -5,6 +5,7 @@ import com.featuremodule.core.navigation.NavCommand
import com.featuremodule.core.navigation.NavManager
import com.featuremodule.core.ui.BaseVM
import com.featuremodule.featureAApi.FeatureADestination
+import com.featuremodule.homeImpl.InternalRoutes
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlin.random.Random
@@ -26,6 +27,12 @@ internal class HomeVM @Inject constructor(
NavCommand.Forward(FeatureADestination.constructRoute(randomInt)),
)
}
+
+ Event.NavigateToExoplayer -> launch {
+ navManager.navigate(
+ NavCommand.Forward(InternalRoutes.ExoplayerDestination.constructRoute()),
+ )
+ }
}
}
}
diff --git a/feature/homeImpl/src/main/res/drawable/pause.xml b/feature/homeImpl/src/main/res/drawable/pause.xml
new file mode 100644
index 0000000..b266200
--- /dev/null
+++ b/feature/homeImpl/src/main/res/drawable/pause.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/feature/homeImpl/src/main/res/drawable/play.xml b/feature/homeImpl/src/main/res/drawable/play.xml
new file mode 100644
index 0000000..c710715
--- /dev/null
+++ b/feature/homeImpl/src/main/res/drawable/play.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/feature/homeImpl/src/main/res/raw/fumo_sail.mp4 b/feature/homeImpl/src/main/res/raw/fumo_sail.mp4
new file mode 100644
index 0000000..cfe3b9f
Binary files /dev/null and b/feature/homeImpl/src/main/res/raw/fumo_sail.mp4 differ
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 63049e3..4eb3191 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -22,6 +22,7 @@ moshi = "1.15.1"
collections-immutable = "0.3.7"
glide-compose = "1.0.0-beta01"
leakcanary = "2.14"
+media3 = "1.4.1"
# Versions used for android{} setup
sdk-compile = "34"
@@ -68,6 +69,9 @@ moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", ver
glide-compose = { module = "com.github.bumptech.glide:compose", version.ref = "glide-compose" }
+media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
+media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3" }
+
# Testing
junit = { module = "junit:junit", version.ref = "junit" }
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junit-androidx" }
@@ -83,6 +87,7 @@ leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version.re
compose = ["androidx-lifecycle-runtime-compose", "compose-ui", "compose-ui-graphics",
"compose-ui-tooling-preview", "compose-material3", "compose-runtime"]
network = ["retrofit", "retrofit-converter-moshi", "moshi"]
+exoplayer = ["media3-exoplayer", "media3-ui"]
[plugins]
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
diff --git a/readme_res/exoplayer_20241115.gif b/readme_res/exoplayer_20241115.gif
new file mode 100644
index 0000000..d96f9c8
Binary files /dev/null and b/readme_res/exoplayer_20241115.gif differ