From f1bd3c98c2006862b7928d47242d0191d813e418 Mon Sep 17 00:00:00 2001 From: Sergei Korotaev Date: Sat, 31 May 2025 13:52:27 +0300 Subject: [PATCH 1/6] Task 1.1 - Timer with coroutine. --- .../ui/timer/TimerFragment.kt | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerFragment.kt b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerFragment.kt index 1b7c0f1..41cbc99 100644 --- a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerFragment.kt +++ b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerFragment.kt @@ -5,10 +5,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.coroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch import ru.otus.coroutineshomework.databinding.FragmentTimerBinding import java.util.Locale @@ -20,6 +20,7 @@ class TimerFragment : Fragment() { private var _binding: FragmentTimerBinding? = null private val binding get() = _binding!! + private var job: Job? = null private var time: Duration by Delegates.observable(Duration.ZERO) { _, _, newValue -> binding.time.text = newValue.toDisplayString() @@ -75,11 +76,18 @@ class TimerFragment : Fragment() { } private fun startTimer() { - // TODO: Start timer + job = lifecycle.coroutineScope.launch { + val startTime = System.currentTimeMillis() + while(true) { + ensureActive() + time = (System.currentTimeMillis()-startTime).milliseconds + delay(15) + } + } } private fun stopTimer() { - // TODO: Stop timer + job?.cancel() } override fun onDestroyView() { @@ -94,9 +102,9 @@ class TimerFragment : Fragment() { private fun Duration.toDisplayString(): String = String.format( Locale.getDefault(), "%02d:%02d.%03d", - this.inWholeMinutes.toInt(), - this.inWholeSeconds.toInt(), - this.inWholeMilliseconds.toInt() + this.inWholeMinutes.toInt()%60, + this.inWholeSeconds.toInt()%60, + this.inWholeMilliseconds.toInt()%1000 ) } } \ No newline at end of file From 4cb14755f0077c47777ef2eb9fb9217f1d6e4b0c Mon Sep 17 00:00:00 2001 From: Sergei Korotaev Date: Sat, 31 May 2025 14:25:15 +0300 Subject: [PATCH 2/6] Task 1.2 - Timer with Flow. --- .../ui/timer/TimerFragment.kt | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerFragment.kt b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerFragment.kt index 41cbc99..233999c 100644 --- a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerFragment.kt +++ b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerFragment.kt @@ -6,14 +6,18 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.coroutineScope +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import ru.otus.coroutineshomework.databinding.FragmentTimerBinding import java.util.Locale import kotlin.properties.Delegates import kotlin.time.Duration +import kotlin.time.Duration.Companion.ZERO import kotlin.time.Duration.Companion.milliseconds class TimerFragment : Fragment() { @@ -22,9 +26,7 @@ class TimerFragment : Fragment() { private val binding get() = _binding!! private var job: Job? = null - private var time: Duration by Delegates.observable(Duration.ZERO) { _, _, newValue -> - binding.time.text = newValue.toDisplayString() - } + private var timeFlow: MutableStateFlow = MutableStateFlow(ZERO) private var started by Delegates.observable(false) { _, _, newValue -> setButtonsState(newValue) @@ -54,12 +56,18 @@ class TimerFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) savedInstanceState?.let { - time = it.getLong(TIME).milliseconds + timeFlow = MutableStateFlow(it.getLong(TIME).milliseconds) started = it.getBoolean(STARTED) } setButtonsState(started) with(binding) { - time.text = this@TimerFragment.time.toDisplayString() + lifecycleScope.launch { + viewLifecycleOwner.lifecycle.repeatOnLifecycle(androidx.lifecycle.Lifecycle.State.STARTED) { + timeFlow.collect { duration-> + time.text = duration.toDisplayString() + } + } + } btnStart.setOnClickListener { started = true } @@ -71,7 +79,7 @@ class TimerFragment : Fragment() { override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putLong(TIME, time.inWholeMilliseconds) + outState.putLong(TIME, timeFlow.value.inWholeMilliseconds) outState.putBoolean(STARTED, started) } @@ -80,7 +88,7 @@ class TimerFragment : Fragment() { val startTime = System.currentTimeMillis() while(true) { ensureActive() - time = (System.currentTimeMillis()-startTime).milliseconds + timeFlow.emit((System.currentTimeMillis()-startTime).milliseconds) delay(15) } } From 99c9a91e9427dfa7cd73b44f8b91f9a9bffb2f11 Mon Sep 17 00:00:00 2001 From: Sergei Korotaev Date: Sat, 31 May 2025 15:50:48 +0300 Subject: [PATCH 3/6] Task 2.1 - Login. --- .../ui/login/LoginViewModel.kt | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/login/LoginViewModel.kt b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/login/LoginViewModel.kt index 5fae38a..28ab713 100644 --- a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/login/LoginViewModel.kt +++ b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/login/LoginViewModel.kt @@ -1,10 +1,17 @@ package ru.otus.coroutineshomework.ui.login +import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import ru.otus.coroutineshomework.ui.login.data.Credentials +import ru.otus.coroutineshomework.ui.login.data.User -class LoginViewModel : ViewModel() { +class LoginViewModel(private val loginApi: LoginApi = LoginApi()) : ViewModel() { private val _state = MutableLiveData(LoginViewState.Login()) val state: LiveData = _state @@ -15,13 +22,46 @@ class LoginViewModel : ViewModel() { * @param password user password */ fun login(name: String, password: String) { - // TODO: Implement login + viewModelScope.launch { + _state.value = LoginViewState.LoggingIn + Log.i("login state", _state.value.toString()) + try { + if (name == "main") { + loginApi.login(Credentials(name, password)) + } + else { + val user: User = withContext(Dispatchers.IO) { + loginApi.login(Credentials(name, password)) + } + _state.value = LoginViewState.Content(user) + Log.i("login state", _state.value.toString()) + } + } + catch (exception: Exception){ + _state.value = LoginViewState.Login(error = exception) + Log.i("login state", _state.value.toString()) + } + } } /** * Logout from the network */ fun logout() { - // TODO: Implement logout + viewModelScope.launch { + _state.value = LoginViewState.LoggingOut + Log.i("login state", _state.value.toString()) + try { + withContext(Dispatchers.IO) { + loginApi.logout() + } + _state.value =LoginViewState.Login() + Log.i("login state", _state.value.toString()) + } + catch (exception: Exception){ + _state.value = LoginViewState.Login(error = exception) + Log.i("login state", _state.value.toString()) + } + } } } From 10e08bbb4502daaab4ee537aee8c028b6613dde2 Mon Sep 17 00:00:00 2001 From: Sergei Korotaev Date: Sat, 31 May 2025 16:49:23 +0300 Subject: [PATCH 4/6] Task 2.2 - Login with Flow. --- .../ui/login/LoginFragment.kt | 16 +++-- .../ui/login/LoginViewModel.kt | 67 +++++++++---------- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/login/LoginFragment.kt b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/login/LoginFragment.kt index 06c3afe..4713edd 100644 --- a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/login/LoginFragment.kt +++ b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/login/LoginFragment.kt @@ -7,6 +7,8 @@ import android.view.ViewGroup import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch import ru.otus.coroutineshomework.databinding.ContentBinding import ru.otus.coroutineshomework.databinding.FragmentLoginBinding import ru.otus.coroutineshomework.databinding.LoadingBinding @@ -43,12 +45,14 @@ class LoginFragment : Fragment() { setupLogin() setupContent() - loginViewModel.state.observe(viewLifecycleOwner) { - when(it) { - is LoginViewState.Login -> showLogin(it) - LoginViewState.LoggingIn -> showLoggingIn() - is LoginViewState.Content -> showContent(it) - LoginViewState.LoggingOut -> showLoggingOut() + lifecycleScope.launch { + loginViewModel.state.collect { state -> + when (state) { + is LoginViewState.Login -> showLogin(state) + LoginViewState.LoggingIn -> showLoggingIn() + is LoginViewState.Content -> showContent(state) + LoginViewState.LoggingOut -> showLoggingOut() + } } } } diff --git a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/login/LoginViewModel.kt b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/login/LoginViewModel.kt index 28ab713..49528be 100644 --- a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/login/LoginViewModel.kt +++ b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/login/LoginViewModel.kt @@ -1,11 +1,12 @@ package ru.otus.coroutineshomework.ui.login -import android.util.Log -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ru.otus.coroutineshomework.ui.login.data.Credentials @@ -13,8 +14,31 @@ import ru.otus.coroutineshomework.ui.login.data.User class LoginViewModel(private val loginApi: LoginApi = LoginApi()) : ViewModel() { - private val _state = MutableLiveData(LoginViewState.Login()) - val state: LiveData = _state + private val stateFlow = MutableStateFlow(LoginViewState.Login()) + val state: StateFlow = stateFlow + + private fun loginFlow(credentials: Credentials?): Flow { + return flow { + try { + if (credentials != null) { + emit(LoginViewState.LoggingIn) + val user: User = withContext(Dispatchers.IO) { + loginApi.login(credentials) + } + emit(LoginViewState.Content(user)) + } else { + emit(LoginViewState.LoggingOut) + withContext(Dispatchers.IO) { + loginApi.logout() + } + emit(LoginViewState.Login()) + } + } + catch (exception: Exception) { + emit(LoginViewState.Login(error = exception)) + } + } + } /** * Login to the network @@ -23,23 +47,8 @@ class LoginViewModel(private val loginApi: LoginApi = LoginApi()) : ViewModel() */ fun login(name: String, password: String) { viewModelScope.launch { - _state.value = LoginViewState.LoggingIn - Log.i("login state", _state.value.toString()) - try { - if (name == "main") { - loginApi.login(Credentials(name, password)) - } - else { - val user: User = withContext(Dispatchers.IO) { - loginApi.login(Credentials(name, password)) - } - _state.value = LoginViewState.Content(user) - Log.i("login state", _state.value.toString()) - } - } - catch (exception: Exception){ - _state.value = LoginViewState.Login(error = exception) - Log.i("login state", _state.value.toString()) + loginFlow(Credentials(name, password)).collect { + stateFlow.value = it } } } @@ -49,18 +58,8 @@ class LoginViewModel(private val loginApi: LoginApi = LoginApi()) : ViewModel() */ fun logout() { viewModelScope.launch { - _state.value = LoginViewState.LoggingOut - Log.i("login state", _state.value.toString()) - try { - withContext(Dispatchers.IO) { - loginApi.logout() - } - _state.value =LoginViewState.Login() - Log.i("login state", _state.value.toString()) - } - catch (exception: Exception){ - _state.value = LoginViewState.Login(error = exception) - Log.i("login state", _state.value.toString()) + loginFlow(null).collect { + stateFlow.value = it } } } From 18963357954481292ff955ddcb7be35c36fb646c Mon Sep 17 00:00:00 2001 From: Sergei Korotaev Date: Sat, 31 May 2025 17:59:30 +0300 Subject: [PATCH 5/6] Task 3 - Speedtest. --- app/build.gradle.kts | 1 + .../ui/network/NetworkViewModel.kt | 22 ++++++++++++++++++- gradle/libs.versions.toml | 18 ++++++++------- gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f6d9019..7a2b0a5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -37,6 +37,7 @@ android { } dependencies { + implementation(libs.androidx.media3.common.ktx) coreLibraryDesugaring(libs.desugar) implementation(libs.androidx.core.ktx) diff --git a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/network/NetworkViewModel.kt b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/network/NetworkViewModel.kt index f006e03..d90629e 100644 --- a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/network/NetworkViewModel.kt +++ b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/network/NetworkViewModel.kt @@ -4,8 +4,12 @@ import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.random.Random @@ -18,7 +22,23 @@ class NetworkViewModel : ViewModel() { val result: LiveData = _result fun startTest(numberOfThreads: Int) { - // TODO: Implement the logic + viewModelScope.launch { + val resultList = mutableListOf>>() + _running.value = true + repeat(numberOfThreads) { + resultList.add(async(Dispatchers.IO) { + emulateBlockingNetworkRequest() + }) + } + val results = resultList.mapNotNull { it.await().getOrNull() } + _running.value = false + + if (results.isNotEmpty()) { + _result.value = results.average().toLong() + } else { + _result.value = null + } + } } private companion object { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 866702f..d87e290 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,19 +1,20 @@ [versions] -agp = "8.7.3" +agp = "8.10.1" kotlin = "2.1.0" -coreKtx = "1.15.0" +coreKtx = "1.16.0" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" appcompat = "1.7.0" material = "1.12.0" -constraintlayout = "2.2.0" -lifecycleLivedataKtx = "2.8.7" -lifecycleViewmodelKtx = "2.8.7" -navigationFragmentKtx = "2.8.5" -navigationUiKtx = "2.8.5" +constraintlayout = "2.2.1" +lifecycleLivedataKtx = "2.9.0" +lifecycleViewmodelKtx = "2.9.0" +navigationFragmentKtx = "2.9.0" +navigationUiKtx = "2.9.0" coroutines = "1.9.0" -desugar = "1.1.5" +desugar = "2.1.5" +media3CommonKtx = "1.7.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -29,6 +30,7 @@ androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navi androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } desugar = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugar" } +androidx-media3-common-ktx = { group = "androidx.media3", name = "media3-common-ktx", version.ref = "media3CommonKtx" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f469e3b..3313c4d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Jan 07 11:05:09 CET 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 10f1644ee9ada55cfef080c95c0a32b8af7f0dd9 Mon Sep 17 00:00:00 2001 From: Sergei Korotaev Date: Wed, 4 Jun 2025 21:00:45 +0300 Subject: [PATCH 6/6] Task 1.2 & 3 refactored. --- .../ui/network/NetworkViewModel.kt | 5 ++++- .../ui/timer/TimerFragment.kt | 19 +++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/network/NetworkViewModel.kt b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/network/NetworkViewModel.kt index d90629e..2f3d503 100644 --- a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/network/NetworkViewModel.kt +++ b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/network/NetworkViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -30,7 +31,9 @@ class NetworkViewModel : ViewModel() { emulateBlockingNetworkRequest() }) } - val results = resultList.mapNotNull { it.await().getOrNull() } + + val results = resultList.awaitAll().mapNotNull { it.getOrNull() } + _running.value = false if (results.isNotEmpty()) { diff --git a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerFragment.kt b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerFragment.kt index 233999c..7d375d2 100644 --- a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerFragment.kt +++ b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerFragment.kt @@ -10,11 +10,14 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import ru.otus.coroutineshomework.databinding.FragmentTimerBinding import java.util.Locale +import kotlin.coroutines.coroutineContext import kotlin.properties.Delegates import kotlin.time.Duration import kotlin.time.Duration.Companion.ZERO @@ -83,13 +86,17 @@ class TimerFragment : Fragment() { outState.putBoolean(STARTED, started) } + private fun createTimerFlow(initial: Long): Flow = flow { + while(coroutineContext.isActive) { + emit((System.currentTimeMillis()-initial).milliseconds) + delay(15) + } + } + private fun startTimer() { job = lifecycle.coroutineScope.launch { - val startTime = System.currentTimeMillis() - while(true) { - ensureActive() - timeFlow.emit((System.currentTimeMillis()-startTime).milliseconds) - delay(15) + createTimerFlow(System.currentTimeMillis()).collect { + timeFlow.value = it } } }