Skip to content

Commit

Permalink
Refactor: Improve UiLessonScreen state management and player lifecycle
Browse files Browse the repository at this point in the history
- Removed redundant private state variables (_isPlaying, _playbackDuration, _playbackPosition).
- Updated UiLessonScreen state directly within the StateFlow for playback state changes.
- Ensured proper player lifecycle management.

Key improvements:

- Made UiLessonScreen reactive by observing state via StateFlow.
- Managed isPlaying and playbackPosition updates via the ViewModel.
- Fixed LaunchedEffect and improper delegation usage in the Composable.
  • Loading branch information
Mihai-Cristian Condrea committed Dec 5, 2024
1 parent 63afa1a commit cbe1e2d
Show file tree
Hide file tree
Showing 13 changed files with 97 additions and 133 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Version 5.1.0:
- Fixed lessons not loading, added a new api for that that is more dynamic
- Fixed the entire project building, now it should work with no probems and see the lessons
- Fixed the entire project building, now it should work with no problems and see the lessons
- Updated the project's dependencies
- Added a new about libraries screen with eula and changelog info
- Removed unused stuff from the project, now the app should be more clear
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
</activity>

<activity
android:name=".ui.screens.lessons.LessonsActivity"
android:name=".ui.screens.lessons.LessonActivity"
android:parentActivityName=".ui.screens.main.MainActivity" />

<activity
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.d4rk.englishwithlidia.plus.data.model.ui.screens.home

data class UiLessonScreen(
val isPlaying : Boolean = false ,
val playbackPosition : Long = 0L ,
val playbackDuration : Long = 0L ,
val lessonTitle : String = "" ,
val lessonContent : List<UiLessonContent> = emptyList()
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package com.d4rk.englishwithlidia.plus.ui.components.layouts

import android.content.Context
import android.os.Build.VERSION.SDK_INT
import android.widget.Toast
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ScrollState
Expand All @@ -15,52 +12,36 @@ 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.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.outlined.CopyAll
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import coil3.ImageLoader
import coil3.compose.AsyncImage
import com.d4rk.englishwithlidia.plus.constants.ui.lessons.LessonContentTypes
import com.d4rk.englishwithlidia.plus.data.datastore.DataStore
import com.d4rk.englishwithlidia.plus.data.model.ui.screens.home.UiLessonContent
import com.d4rk.englishwithlidia.plus.data.model.ui.screens.home.UiLessonScreen
import com.d4rk.englishwithlidia.plus.ui.components.ads.AdBanner
import com.d4rk.englishwithlidia.plus.ui.components.ads.LargeBannerAdsComposable
import com.d4rk.englishwithlidia.plus.ui.components.animations.bounceClick
import com.d4rk.englishwithlidia.plus.ui.screens.home.HomeViewModel
import com.d4rk.englishwithlidia.plus.ui.screens.lessons.LessonsViewModel
import com.d4rk.englishwithlidia.plus.ui.screens.lessons.LessonViewModel
import com.d4rk.englishwithlidia.plus.ui.screens.settings.display.theme.style.Colors
import com.d4rk.englishwithlidia.plus.ui.screens.settings.display.theme.style.TextStyles
import ir.mahozad.multiplatform.wavyslider.WaveDirection
Expand All @@ -72,7 +53,7 @@ fun LessonContentLayout(
scrollState : ScrollState ,
dataStore : DataStore ,
lesson : UiLessonScreen ,
viewModel : LessonsViewModel ,
viewModel : LessonViewModel ,
) {
Column(
modifier = Modifier
Expand Down Expand Up @@ -100,12 +81,13 @@ fun LessonContentLayout(
}

LessonContentTypes.CONTENT_PLAYER -> {
val sliderPosition by viewModel.playbackPosition.collectAsState()
val playbackDuration by viewModel.playbackDuration.collectAsState()
val isPlaying by viewModel.isPlaying.collectAsState()
val sliderPosition = lesson.playbackPosition
val playbackDuration = lesson.playbackDuration

LaunchedEffect(key1 = lesson.lessonContent.contentAudioUrl) {
viewModel.preparePlayer(lessonDetails.UiLessonContent.audioUrl)
val isPlaying = lesson.isPlaying

LaunchedEffect(key1 = contentItem.contentAudioUrl) {
viewModel.preparePlayer(contentItem.contentAudioUrl)
}

AudioCardView(onPlayClick = { viewModel.playPause() } ,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import com.d4rk.englishwithlidia.plus.ui.components.animations.animateVisibility
import com.d4rk.englishwithlidia.plus.ui.components.animations.bounceClick
import com.d4rk.englishwithlidia.plus.ui.components.buttons.OutlinedUrlButtons
import com.d4rk.englishwithlidia.plus.ui.components.drawable.homeBanner
import com.d4rk.englishwithlidia.plus.ui.screens.lessons.LessonsActivity
import com.d4rk.englishwithlidia.plus.ui.screens.lessons.LessonActivity

@Composable
fun LessonListLayout(
Expand Down Expand Up @@ -124,7 +124,7 @@ fun LessonItem(lesson : UiHomeLesson , context : Context , modifier : Modifier =
LessonCard(title = lesson.lessonTitle ,
imageResource = lesson.lessonThumbnailImageUrl ,
onClick = {
val intent = Intent(context , LessonsActivity::class.java).apply {
val intent = Intent(context , LessonActivity::class.java).apply {
putExtra("lessonDetails" , lesson.lessonId)
}
context.startActivity(intent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.d4rk.englishwithlidia.plus.ui.screens.home.repository

import android.app.Application
import com.d4rk.englishwithlidia.plus.data.datastore.DataStore
import com.d4rk.englishwithlidia.plus.data.model.ui.screens.UiLessonsAsset
import com.d4rk.englishwithlidia.plus.data.model.ui.screens.home.UiHomeScreen
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.d4rk.englishwithlidia.plus.ui.screens.lessons

import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.d4rk.englishwithlidia.plus.ui.screens.settings.display.theme.style.AppTheme

class LessonActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()

setContent {
AppTheme {
Surface(
modifier = Modifier.fillMaxSize() , color = MaterialTheme.colorScheme.background
) {
LessonScreen(activity = this@LessonActivity)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,29 @@ package com.d4rk.englishwithlidia.plus.ui.screens.lessons
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import com.d4rk.englishwithlidia.plus.R
import com.d4rk.englishwithlidia.plus.data.datastore.DataStore
import com.d4rk.englishwithlidia.plus.data.model.ui.error.UiErrorModel
import com.d4rk.englishwithlidia.plus.ui.components.dialogs.ErrorAlertDialog
import com.d4rk.englishwithlidia.plus.ui.components.layouts.LessonListLayout
import com.d4rk.englishwithlidia.plus.ui.components.layouts.LessonContentLayout
import com.d4rk.englishwithlidia.plus.ui.components.layouts.LoadingScreen
import com.d4rk.englishwithlidia.plus.ui.components.navigation.TopAppBarScaffoldWithBackButton

@Composable
fun LessonsComposable(viewModel : LessonsViewModel , activity : LessonsActivity) {
fun LessonScreen(activity : LessonActivity) {
val viewModel : LessonViewModel = viewModel()
val context = LocalContext.current
val dataStore = DataStore.getInstance(context)
val uiErrorModel : UiErrorModel by viewModel.uiErrorModel.collectAsState()
val uiState by viewModel.uiState.collectAsState()
val visibilityStates by viewModel.visibilityStates.collectAsState()
val scrollState = rememberScrollState()
val isLoading by viewModel.isLoading.collectAsState()

val transition : Transition<Boolean> =
Expand All @@ -40,8 +45,12 @@ fun LessonsComposable(viewModel : LessonsViewModel , activity : LessonsActivity)
LoadingScreen(progressAlpha)
}
else {
LessonListLayout(
lessons = uiState.lessons , context = context , visibilityStates = visibilityStates
LessonContentLayout(
paddingValues = paddingValues ,
scrollState = scrollState ,
dataStore = dataStore ,
lesson = uiState ,
viewModel = viewModel
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,34 @@ package com.d4rk.englishwithlidia.plus.ui.screens.lessons

import android.app.Application
import android.net.Uri
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import com.d4rk.englishwithlidia.plus.data.model.ui.screens.UiLessonsAsset
import com.d4rk.englishwithlidia.plus.data.datastore.DataStore
import com.d4rk.englishwithlidia.plus.data.model.ui.screens.home.UiLessonScreen
import com.d4rk.englishwithlidia.plus.ui.screens.home.repository.LessonRepository
import com.d4rk.englishwithlidia.plus.ui.viewmodel.BaseViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class LessonsViewModel(application: Application) : BaseViewModel(application) {
class LessonViewModel(application : Application) : BaseViewModel(application) {
private val repository = LessonRepository(DataStore(application) , application)

private val _isPlaying = MutableStateFlow(false)
val isPlaying: StateFlow<Boolean> = _isPlaying.asStateFlow()
private val _uiState = MutableStateFlow(UiLessonScreen())
val uiState : StateFlow<UiLessonScreen> = _uiState

private val _playbackPosition = MutableStateFlow(0L)
val playbackPosition: StateFlow<Long> = _playbackPosition.asStateFlow()

private val _playbackDuration = MutableStateFlow(0L)
val playbackDuration: StateFlow<Long> = _playbackDuration.asStateFlow()

var lessonDetails: UiLessonsAsset? by mutableStateOf(lessonDetails)

private var player: Player? = null
private var player : Player? = null

init {
viewModelScope.launch {
player = ExoPlayer.Builder(getApplication()).build()
}
}

fun preparePlayer(audioUrl: String) {
fun preparePlayer(audioUrl : String) {
viewModelScope.launch {
player?.release()

Expand All @@ -51,55 +41,64 @@ class LessonsViewModel(application: Application) : BaseViewModel(application) {
playWhenReady = false

addListener(object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
_isPlaying.value = isPlaying
override fun onIsPlayingChanged(isPlaying : Boolean) {
updateUiState { copy(isPlaying = isPlaying) }
}

override fun onPlaybackStateChanged(playbackState: Int) {
override fun onPlaybackStateChanged(playbackState : Int) {
if (playbackState == Player.STATE_READY) {
_playbackDuration.value = duration
val duration = this@apply.duration
updateUiState { copy(playbackDuration = duration) }
}
}

override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,
reason: Int
oldPosition : Player.PositionInfo ,
newPosition : Player.PositionInfo ,
reason : Int
) {
_playbackPosition.value = currentPosition
val currentPosition = this@apply.currentPosition
updateUiState { copy(playbackPosition = currentPosition) }
}
})
}

startPositionUpdateJob()
}
}

fun playPause() {
player?.let {
if (it.isPlaying) {
it.pause()
} else {
it.playWhenReady = true
player?.let { player ->
if (player.isPlaying) {
player.pause()
}
else {
player.playWhenReady = true
}
}
}

fun seekTo(position: Long) {
fun seekTo(position : Long) {
player?.seekTo(position)
}

private fun startPositionUpdateJob() {
viewModelScope.launch {
while (true) {
_playbackPosition.value = player?.currentPosition ?: 0L
delay(100)
val currentPosition = player?.currentPosition ?: 0L
updateUiState { copy(playbackPosition = currentPosition) }
delay(timeMillis = 100)
if (player?.isPlaying == false) {
break
}
}
}
}

private fun updateUiState(update : UiLessonScreen.() -> UiLessonScreen) {
_uiState.value = _uiState.value.update()
}

override fun onCleared() {
super.onCleared()
player?.release()
Expand Down
Loading

0 comments on commit cbe1e2d

Please sign in to comment.