Skip to content

Instantly share code, notes, and snippets.

@stevdza-san
Created October 15, 2025 15:39
Show Gist options
  • Save stevdza-san/c812ba5ccebbcbc6aac5ba15fac1d297 to your computer and use it in GitHub Desktop.
Save stevdza-san/c812ba5ccebbcbc6aac5ba15fac1d297 to your computer and use it in GitHub Desktop.
Homework - REFACTORED
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.stevdza_san.demo.homework.compose.viewmodel.heroImages
import com.stevdza_san.demo.homework.compose_new.repository.Channel
import com.stevdza_san.demo.homework.compose_new.repository.ChannelCategory
import com.stevdza_san.demo.homework.compose_new.repository.CurrentShow
import com.stevdza_san.demo.homework.compose_new.repository.Program
import com.stevdza_san.demo.homework.compose_new.viewmodel.TvViewModelNew
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.painterResource
@Composable
fun ChannelScreen() {
val viewModel: TvViewModelNew = viewModel()
val currentImageIndex by viewModel.currentImageIndex.collectAsStateWithLifecycle(initialValue = 0)
val currentImage by remember { derivedStateOf { heroImages[currentImageIndex] } }
val channels by viewModel.channels.collectAsStateWithLifecycle()
val filteredPrograms by viewModel.filteredPrograms.collectAsStateWithLifecycle()
val selectedChannel by viewModel.selectedChannel.collectAsStateWithLifecycle()
val channelCategories by viewModel.channelCategories.collectAsStateWithLifecycle()
val selectedChannelCategory by viewModel.selectedChannelCategory.collectAsStateWithLifecycle()
val overlayVisibility by viewModel.overlayVisibility.collectAsStateWithLifecycle()
val currentShow by viewModel.currentShow.collectAsStateWithLifecycle()
Box(
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding()
.background(Color.White),
contentAlignment = Alignment.BottomCenter
) {
Column(
modifier = Modifier.fillMaxSize()
) {
// Hero Banner
HeroBanner(
currentImage = currentImage,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Categories(
categories = channelCategories,
selected = selectedChannelCategory,
onCategoryClick = viewModel::selectCategory
)
Spacer(modifier = Modifier.height(16.dp))
// Content Lists
Row(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(horizontal = 16.dp)
) {
// Channel List
ChannelList(
channels = selectedChannelCategory?.let { category ->
channels.filter { it.categoryId == category.id }
} ?: channels,
selectedChannel = selectedChannel,
onChannelClick = viewModel::selectChannel,
modifier = Modifier
.weight(1f)
.fillMaxHeight()
)
Spacer(modifier = Modifier.width(16.dp))
// Program List
ProgramList(
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
programs = filteredPrograms,
onClick = viewModel::selectProgram
)
}
}
// Current Show Overlay
AnimatedVisibility(
visible = overlayVisibility,
enter = CUSTOM_ENTER_ANIMATION,
exit = CUSTOM_EXIT_ANIMATION,
modifier = Modifier.align(Alignment.BottomCenter)
) {
currentShow?.let { CurrentShowOverlay(currentShow = it) }
}
}
}
@Composable
fun HeroBanner(
currentImage: DrawableResource,
modifier: Modifier = Modifier,
) {
Card(
modifier = modifier
.padding(horizontal = 16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Box(
modifier = Modifier.fillMaxSize()
) {
AnimatedContent(
targetState = currentImage,
transitionSpec = {
fadeIn(animationSpec = tween(800)) togetherWith
fadeOut(animationSpec = tween(800))
}
) { image ->
Image(
painter = painterResource(image),
contentDescription = "Hero Banner",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
}
}
}
@Composable
fun Categories(
categories: List<ChannelCategory>,
selected: ChannelCategory?,
onCategoryClick: (ChannelCategory) -> Unit,
) {
LazyRow(
modifier = Modifier
.fillMaxWidth()
.padding(all = 12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = categories,
key = { it.id }
) {
FilterChip(
selected = it == selected,
label = { Text(text = it.name) },
onClick = { onCategoryClick(it) }
)
}
}
}
@Composable
fun ChannelList(
channels: List<Channel>,
selectedChannel: Channel?,
onChannelClick: (Channel) -> Unit,
modifier: Modifier = Modifier,
) {
Card(
modifier = modifier,
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier.fillMaxSize()
) {
Text(
text = "Channels",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primary)
.padding(16.dp),
color = MaterialTheme.colorScheme.onPrimary,
textAlign = TextAlign.Center
)
AnimatedContent(
targetState = channels,
transitionSpec = {
fadeIn(animationSpec = tween(300)) togetherWith
fadeOut(animationSpec = tween(300))
}
) { channelsData ->
if (channelsData.isNotEmpty()) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(channelsData) { channel ->
ChannelItem(
channel = channel,
isSelected = channel == selectedChannel,
onClick = { onChannelClick(channel) }
)
}
}
} else {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "No channels available",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center
)
}
}
}
}
}
}
@Composable
fun ChannelItem(
channel: Channel,
isSelected: Boolean,
onClick: () -> Unit,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() },
colors = CardDefaults.cardColors(
containerColor = if (isSelected) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surface
}
),
elevation = CardDefaults.cardElevation(
defaultElevation = if (isSelected) 4.dp else 2.dp
)
) {
Text(
text = channel.name,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
fontSize = 14.sp,
color = if (isSelected) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurface
}
)
}
}
@Composable
fun ProgramList(
modifier: Modifier = Modifier,
programs: List<Program>,
onClick: (Program) -> Unit,
) {
Card(
modifier = modifier,
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier.fillMaxSize()
) {
Text(
text = "Programs",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.secondary)
.padding(16.dp),
color = MaterialTheme.colorScheme.onSecondary,
textAlign = TextAlign.Center
)
AnimatedContent(
targetState = programs,
transitionSpec = {
fadeIn(animationSpec = tween(300)) togetherWith
fadeOut(animationSpec = tween(300))
}
) { programList ->
if (programList.isNotEmpty()) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(
items = programList,
key = { program -> program.id }
) { program ->
ProgramItem(
program = program,
onClick = onClick
)
}
}
} else {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Select a channel to view programs",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center
)
}
}
}
}
}
}
@Composable
fun ProgramItem(
program: Program,
onClick: (Program) -> Unit,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick(program) },
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Text(
text = program.name,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface
)
}
}
@Composable
fun CurrentShowOverlay(
currentShow: CurrentShow,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color.Black.copy(alpha = 0.85f)
),
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
) {
// Show title
Text(
text = currentShow.title,
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier.padding(bottom = 8.dp)
)
// Time slot
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 8.dp)
) {
Text(
text = "⏰ ",
fontSize = 16.sp,
color = Color.White.copy(alpha = 0.8f)
)
Text(
text = currentShow.timeSlot,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = Color.White.copy(alpha = 0.9f)
)
}
// Rating
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 12.dp)
) {
Text(
text = currentShow.rating,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFFFFD700) // Gold color for rating
)
}
// Description
Text(
text = currentShow.description,
fontSize = 14.sp,
color = Color.White.copy(alpha = 0.85f),
lineHeight = 20.sp,
textAlign = TextAlign.Justify
)
Spacer(modifier = Modifier.height(8.dp))
// "Now Playing" indicator
Row(
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(8.dp)
.background(
Color.Red,
CircleShape
)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "NOW PLAYING",
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = Color.Red,
letterSpacing = 1.sp
)
}
}
}
}
val CUSTOM_ENTER_ANIMATION = slideInVertically(
animationSpec = tween(400),
initialOffsetY = { it }
) + scaleIn(
animationSpec = tween(400),
initialScale = 0.8f
) + fadeIn(
animationSpec = tween(300)
)
val CUSTOM_EXIT_ANIMATION = slideOutVertically(
animationSpec = tween(300),
targetOffsetY = { it }
) + scaleOut(
animationSpec = tween(300),
targetScale = 0.9f
) + fadeOut(
animationSpec = tween(200)
)
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.delay
import kotlin.collections.listOf
data class Channel(
val id: String,
val name: String,
val categoryId: Int? = null
)
data class ChannelCategory(
val id: Int,
val name: String
)
data class CurrentShow(
val title: String,
val description: String,
val timeSlot: String,
val rating: String,
)
data class Program(
val id: String,
val channelId: String,
val name: String,
)
class FakeTvRepository {
/**
* Returns a Flow that continuously emits shuffled channel lists every 10 seconds.
*
* This function simulates dynamic channel data by:
* 1. Initially emitting the original list of channels
* 2. Every 10 seconds, moving the last channel to the first position
* 3. Emitting the newly shuffled list
* 4. Repeating this rotation indefinitely
*
* Example sequence for channels [A, B, C, D]:
* - Initial: [A, B, C, D]
* - After 10s: [D, A, B, C]
* - After 20s: [C, D, A, B]
* - After 30s: [B, C, D, A]
* - After 40s: [A, B, C, D] (back to original)
*
* This creates a continuous stream of data changes to test UI reactivity
* and simulate real-world scenarios where channel lists might update periodically.
*
* @return Flow<List<Channel>> A cold flow that emits shuffled channel lists every 10 seconds
*/
fun getChannels(): Flow<List<Channel>> = flow {
var channels = fetchChannels()
emit(channels)
while (true) {
delay(10_000) // 10 seconds
if (channels.isNotEmpty()) {
// Move the last element to the first position
channels = listOf(channels.last()) + channels.dropLast(1)
emit(channels)
}
}
}
fun getPrograms(): Flow<List<Program>> = flow {
emit(fetchPrograms())
}
fun getCurrentShows(): Flow<List<CurrentShow>> = flow {
emit(fetchCurrentShowsList())
}
fun getChannelCategories(): Flow<List<ChannelCategory>> = flow {
emit(fetchChannelCategories())
}
private fun fetchChannels(): List<Channel> {
return (1..10).map { id ->
Channel(
id = "$id",
name = "Channel $id",
categoryId = listOf(1, 2, 3).random()
)
}
}
private fun fetchPrograms(): List<Program> {
val programNames = listOf(
"Morning News", "Weather Report", "Talk Show", "Comedy Hour", "Drama Series",
"Documentary", "Sports Update", "Cooking Show", "Reality TV", "Game Show",
"Movie Night", "Late Show", "Kids Program", "Educational", "Music Video",
"Travel Guide", "Science Today", "History Channel", "Nature Watch", "Tech Review"
)
return (1..20).flatMap { channelId ->
programNames.shuffled().mapIndexed { index, baseName ->
Program(
id = "${baseName}_${channelId}_${index}",
channelId = "$channelId",
name = "$baseName ${(1..99).random()}"
)
}
}
}
private fun fetchCurrentShowsList(): List<CurrentShow> {
return listOf(
CurrentShow(
"The Crown",
"A biographical drama about Queen Elizabeth II's reign, exploring personal relationships and political events.",
"8:00 PM - 9:00 PM",
"★★★★☆ 4.2/5"
),
CurrentShow(
"Stranger Things",
"Supernatural horror series set in the 1980s following kids in a small town dealing with mysterious events.",
"9:00 PM - 10:00 PM",
"★★★★★ 4.8/5"
),
CurrentShow(
"Breaking Bad",
"A high school chemistry teacher turned methamphetamine manufacturer in this critically acclaimed crime drama.",
"10:00 PM - 11:00 PM",
"★★★★★ 4.9/5"
),
CurrentShow(
"The Office",
"Mockumentary sitcom depicting the everyday work lives of office employees in Scranton, Pennsylvania.",
"7:30 PM - 8:00 PM",
"★★★★☆ 4.4/5"
),
CurrentShow(
"Game of Thrones",
"Epic fantasy series featuring political intrigue, dragons, and the battle for the Iron Throne.",
"9:00 PM - 10:00 PM",
"★★★★☆ 4.1/5"
),
CurrentShow(
"Friends",
"Classic sitcom following six friends living in Manhattan navigating life, love, and careers.",
"8:00 PM - 8:30 PM",
"★★★★☆ 4.5/5"
),
CurrentShow(
"The Mandalorian",
"Star Wars series following a bounty hunter's adventures in the outer reaches of the galaxy.",
"8:00 PM - 9:00 PM",
"★★★★☆ 4.3/5"
),
CurrentShow(
"Squid Game",
"Korean survival thriller where contestants play deadly children's games for a massive cash prize.",
"9:00 PM - 10:00 PM",
"★★★★☆ 4.2/5"
),
CurrentShow(
"The Witcher",
"Fantasy series following Geralt of Rivia, a monster hunter in a world full of magic and political intrigue.",
"8:00 PM - 9:00 PM",
"★★★★☆ 4.0/5"
),
CurrentShow(
"Wednesday",
"Dark comedy following Wednesday Addams as she navigates her years as a student at Nevermore Academy.",
"7:00 PM - 8:00 PM",
"★★★★☆ 4.1/5"
)
)
}
private fun fetchChannelCategories(): List<ChannelCategory> {
return listOf(
ChannelCategory(1, "Sport"),
ChannelCategory(2, "News"),
ChannelCategory(3, "Entertainment")
)
}
}
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.stevdza_san.demo.homework.compose_new.repository.Channel
import com.stevdza_san.demo.homework.compose_new.repository.ChannelCategory
import com.stevdza_san.demo.homework.compose_new.repository.FakeTvRepository
import com.stevdza_san.demo.homework.compose_new.repository.Program
import demo.composeapp.generated.resources.Res
import demo.composeapp.generated.resources.hero1
import demo.composeapp.generated.resources.hero2
import demo.composeapp.generated.resources.hero3
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlin.collections.filter
val heroImages = listOf(
Res.drawable.hero1,
Res.drawable.hero2,
Res.drawable.hero3
)
class TvViewModelNew : ViewModel() {
private val repository = FakeTvRepository()
val selectedChannel: StateFlow<Channel?>
field = MutableStateFlow(null)
val selectedChannelCategory: StateFlow<ChannelCategory?>
field = MutableStateFlow(null)
private val selectedProgram: StateFlow<Program?>
field = MutableStateFlow(null)
val currentImageIndex = flow {
var index = 0
while (true) {
emit(index)
delay(5000)
index = (index + 1) % heroImages.size
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = 0
)
val channels = repository.getChannels()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
val channelCategories = repository.getChannelCategories()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
private val programs = repository.getPrograms()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
// Public: Programs filtered by selected channel
@OptIn(ExperimentalCoroutinesApi::class)
val filteredPrograms = combine(selectedChannel, programs) { channel, allPrograms ->
channel?.let {
allPrograms.filter { it.channelId == channel.id }
} ?: emptyList()
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
val currentShow = selectedProgram
.filterNotNull()
.flatMapLatest { program ->
repository.getCurrentShows()
.map { shows -> shows.shuffled().firstOrNull() }
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = null
)
@OptIn(ExperimentalCoroutinesApi::class)
val overlayVisibility = selectedProgram
.filterNotNull()
.flatMapLatest { programId ->
flow {
emit(true) // Show overlay
delay(3000) // Wait 3 seconds
emit(false) // Hide overlay
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = false
)
fun selectChannel(channel: Channel) {
selectedChannel.value = channel
}
fun selectProgram(program: Program) {
selectedProgram.value = program
}
fun selectCategory(category: ChannelCategory) {
selectedChannelCategory.value = category
// Improve consistency
selectedChannel.value = null
selectedProgram.value = null
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment