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 cbe1e2d commit e7ad933
Show file tree
Hide file tree
Showing 14 changed files with 167 additions and 25 deletions.
14 changes: 13 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,19 @@

<activity
android:name=".ui.screens.lessons.LessonActivity"
android:parentActivityName=".ui.screens.main.MainActivity" />
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="lesson"
android:pathPrefix="/"
android:scheme="com.d4rk.englishwithlidia.plus" />
</intent-filter>
</activity>

<activity
android:name=".ui.screens.support.SupportActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ object LessonContentTypes {
const val IMAGE = "image"
const val CONTENT_PLAYER = "content_player"
const val AD_BANNER = "ad_banner"
const val TYPE_DIVIDER = "content_divider"
const val AD_BANNER_FULL = "ad_banner_full"
const val AD_LARGE_BANNER = "ad_large_banner"
const val FULL_IMAGE_BANNER = "full_image_banner"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ data class UiLessonScreen(
val playbackPosition : Long = 0L ,
val playbackDuration : Long = 0L ,
val lessonTitle : String = "" ,
val lessonContent : List<UiLessonContent> = emptyList()
val lessonContent : ArrayList<UiLessonContent> = ArrayList()
)

data class UiLessonContent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ fun LessonContentLayout(
sliderPosition = sliderPosition.toFloat() / 1000f ,
playbackDuration = playbackDuration.toFloat() / 1000f ,
isPlaying = isPlaying)
}

LessonContentTypes.TYPE_DIVIDER -> {
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +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.components.navigation.openLessonDetailActivity
import com.d4rk.englishwithlidia.plus.ui.screens.lessons.LessonActivity

@Composable
Expand Down Expand Up @@ -114,20 +115,19 @@ fun LessonItem(lesson : UiHomeLesson , context : Context , modifier : Modifier =

LessonConstants.TYPE_AD_VIEW_BANNER -> {
AdBanner(modifier = Modifier.fillMaxWidth() , dataStore = dataStore)
Spacer(modifier = Modifier.height(16.dp))
}

LessonConstants.TYPE_AD_VIEW_BANNER_LARGE -> {
LargeBannerAdsComposable(dataStore = dataStore)
Spacer(modifier = Modifier.height(16.dp))
}

LessonConstants.TYPE_FULL_IMAGE_BANNER -> {
LessonCard(title = lesson.lessonTitle ,
imageResource = lesson.lessonThumbnailImageUrl ,
onClick = {
val intent = Intent(context , LessonActivity::class.java).apply {
putExtra("lessonDetails" , lesson.lessonId)
}
context.startActivity(intent)
openLessonDetailActivity(context = context , lesson = lesson)
} ,
modifier = modifier)
Spacer(modifier = Modifier.width(8.dp))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.d4rk.englishwithlidia.plus.ui.components.navigation

import android.content.Context
import android.content.Intent
import android.net.Uri
import com.d4rk.englishwithlidia.plus.data.model.ui.screens.home.UiHomeLesson

fun openLessonDetailActivity(context : Context , lesson : UiHomeLesson) {
println("English with Lidia -> openLessonDetailActivity")

println("English with Lidia -> openLessonDetailActivity checking deep link path -> ${lesson.lessonDeepLinkPath}")

Intent(Intent.ACTION_VIEW , Uri.parse(lesson.lessonDeepLinkPath)).let { intent ->
println("English with Lidia -> openLessonDetailActivity intent -> $intent")
intent.resolveActivity(context.packageManager)?.let {
println("English with Lidia -> openLessonDetailActivity start activity -> $it")
context.startActivity(intent)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import android.app.Application
import androidx.lifecycle.viewModelScope
import com.d4rk.englishwithlidia.plus.data.datastore.DataStore
import com.d4rk.englishwithlidia.plus.data.model.ui.screens.home.UiHomeScreen
import com.d4rk.englishwithlidia.plus.ui.screens.home.repository.LessonRepository
import com.d4rk.englishwithlidia.plus.ui.screens.home.repository.HomeRepository
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.launch

class HomeViewModel(application : Application) : BaseViewModel(application) {
private val repository = LessonRepository(DataStore(application) , application)
private val repository = HomeRepository(DataStore(application) , application)

private val _uiState = MutableStateFlow(UiHomeScreen())
val uiState: StateFlow<UiHomeScreen> = _uiState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import com.d4rk.englishwithlidia.plus.data.model.ui.screens.home.UiHomeScreen
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class LessonRepository(
class HomeRepository(
dataStore : DataStore , application : Application ,
) : LessonRepositoryImplementation(application , dataStore) {
) : HomeRepositoryImplementation(application , dataStore) {

suspend fun getHomeLessonsRepository(onSuccess : (UiHomeScreen) -> Unit) {
withContext(Dispatchers.IO) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import kotlinx.serialization.json.Json

abstract class LessonRepositoryImplementation(
abstract class HomeRepositoryImplementation(
val application : Application ,
val dataStore : DataStore ,
) {
Expand All @@ -37,6 +37,7 @@ abstract class LessonRepositoryImplementation(
val lessons = jsonString.takeUnless { it.isBlank() }
?.let { jsonParser.decodeFromString<ApiHomeResponse>(it) }
?.takeIf { it.data.isNotEmpty() }?.data?.mapNotNull { networkLesson ->
println("English with Lidia Plus -> Fetched lesson: $networkLesson")
runCatching {
UiHomeLesson(
lessonId = networkLesson.lessonId ,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,39 @@ package com.d4rk.englishwithlidia.plus.ui.screens.lessons
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import com.d4rk.englishwithlidia.plus.ui.screens.settings.display.theme.style.AppTheme

class LessonActivity : AppCompatActivity() {
private val viewModel : LessonViewModel by viewModels()

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

setContent {
val lessonId = intent?.data?.lastPathSegment
println("English with Lidia Plus -> lesson id got is: $lessonId")
lessonId?.let { lesson ->
viewModel.getLesson(lessonId = lesson)
}

AppTheme {
Surface(
modifier = Modifier.fillMaxSize() , color = MaterialTheme.colorScheme.background
) {
LessonScreen(activity = this@LessonActivity)
val lesson = viewModel.uiState.collectAsState()
LessonScreen(
activity = this@LessonActivity ,
viewModel = viewModel ,
lesson = lesson.value ,
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@ 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.data.model.ui.screens.home.UiLessonScreen
import com.d4rk.englishwithlidia.plus.ui.components.dialogs.ErrorAlertDialog
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 LessonScreen(activity : LessonActivity) {
val viewModel : LessonViewModel = viewModel()
fun LessonScreen(
activity : LessonActivity ,
viewModel : LessonViewModel ,
lesson : UiLessonScreen
) {
val context = LocalContext.current
val dataStore = DataStore.getInstance(context)
val uiErrorModel : UiErrorModel by viewModel.uiErrorModel.collectAsState()
val uiState by viewModel.uiState.collectAsState()
val scrollState = rememberScrollState()
val isLoading by viewModel.isLoading.collectAsState()

Expand All @@ -34,8 +34,9 @@ fun LessonScreen(activity : LessonActivity) {
if (it) 0f else 1f
}

TopAppBarScaffoldWithBackButton(title = stringResource(id = R.string.security_and_privacy) ,
onBackClicked = { activity.finish() }) { paddingValues ->
TopAppBarScaffoldWithBackButton(
title = lesson.lessonTitle ,
onBackClicked = { activity.finish() }) { paddingValues ->
if (uiErrorModel.showErrorDialog) {
ErrorAlertDialog(errorMessage = uiErrorModel.errorMessage ,
onDismiss = { viewModel.dismissErrorDialog() })
Expand All @@ -49,7 +50,7 @@ fun LessonScreen(activity : LessonActivity) {
paddingValues = paddingValues ,
scrollState = scrollState ,
dataStore = dataStore ,
lesson = uiState ,
lesson = lesson ,
viewModel = viewModel
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,20 @@ import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
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.screens.lessons.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 LessonViewModel(application : Application) : BaseViewModel(application) {
private val repository = LessonRepository(DataStore(application) , application)

private val _uiState = MutableStateFlow(UiLessonScreen())
val uiState : StateFlow<UiLessonScreen> = _uiState
val uiState : StateFlow<UiLessonScreen> = _uiState.asStateFlow()


private var player : Player? = null

Expand All @@ -29,6 +31,16 @@ class LessonViewModel(application : Application) : BaseViewModel(application) {
}
}

fun getLesson(lessonId : String) {
viewModelScope.launch(coroutineExceptionHandler) {
showLoading()
repository.getLessonRepository(lessonId) { fetchedLesson ->
_uiState.value = fetchedLesson
}
hideLoading()
}
}

fun preparePlayer(audioUrl : String) {
viewModelScope.launch {
player?.release()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
package com.d4rk.englishwithlidia.plus.ui.screens.lessons.repository

class LessonRepository {}
import android.app.Application
import com.d4rk.englishwithlidia.plus.data.datastore.DataStore
import com.d4rk.englishwithlidia.plus.data.model.ui.screens.home.UiLessonScreen
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class LessonRepository(
dataStore : DataStore , application : Application ,
) : LessonRepositoryImplementation(application , dataStore) {

suspend fun getLessonRepository(
lessonId : String , onSuccess : (UiLessonScreen) -> Unit
) {
withContext(Dispatchers.IO) {
val lesson = getLessonImplementation(lessonId = lessonId)
withContext(Dispatchers.Main) {
onSuccess(lesson)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,61 @@
package com.d4rk.englishwithlidia.plus.ui.screens.lessons.repository

class LessonRepositoryImplementation {}
import android.app.Application
import com.d4rk.englishwithlidia.plus.BuildConfig
import com.d4rk.englishwithlidia.plus.constants.api.ApiConstants
import com.d4rk.englishwithlidia.plus.data.core.AppCoreManager
import com.d4rk.englishwithlidia.plus.data.datastore.DataStore
import com.d4rk.englishwithlidia.plus.data.model.api.ApiLessonResponse
import com.d4rk.englishwithlidia.plus.data.model.ui.screens.home.UiLessonContent
import com.d4rk.englishwithlidia.plus.data.model.ui.screens.home.UiLessonScreen
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import kotlinx.serialization.json.Json

abstract class LessonRepositoryImplementation(
val application : Application ,
val dataStore : DataStore ,
) {

private val client : HttpClient = AppCoreManager.ktorClient

private val baseUrl = BuildConfig.DEBUG.let { isDebug ->
val environment = if (isDebug) "debug" else "release"
"${ApiConstants.BASE_REPOSITORY_URL}/$environment/ro/lessons"
}

private val jsonParser = Json {
ignoreUnknownKeys = true
isLenient = true
}

suspend fun getLessonImplementation(lessonId : String) : UiLessonScreen {
return runCatching {
val url = "$baseUrl/api_get_$lessonId.json"
val response = client.get(url)
val jsonString = response.bodyAsText()

val lessons = jsonString.takeUnless { it.isBlank() }
?.let { jsonParser.decodeFromString<ApiLessonResponse>(it) }
?.takeIf { it.data.isNotEmpty() }?.data?.map { networkLesson ->
UiLessonScreen(lessonTitle = networkLesson.lessonTitle ,
lessonContent = ArrayList(networkLesson.lessonContent.map { networkContent ->
UiLessonContent(
contentId = networkContent.contentId ,
contentType = networkContent.contentType ,
contentText = networkContent.contentText ,
contentAudioUrl = networkContent.contentAudioUrl ,
contentImageUrl = networkContent.contentImageUrl
)
}))
}?.also { lessons ->
println("Fetched ${lessons.size} lessons")
} ?: emptyList()
lessons.firstOrNull() ?: UiLessonScreen()
}.getOrElse { exception ->
println("Error: ${exception.message}")
UiLessonScreen()
}
}
}

0 comments on commit e7ad933

Please sign in to comment.