From 1b30e212c8509b53a61a426d84ed83d6ee353b28 Mon Sep 17 00:00:00 2001 From: Huawei Date: Sat, 7 Jun 2025 13:23:57 +0300 Subject: [PATCH 1/5] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=201.1=20-=20=D0=A2=D0=B0=D0=B9=D0=BC=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/timer/TimerFragment.kt | 18 +++++++++++------- gradle/libs.versions.toml | 2 ++ 2 files changed, 13 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..b90004f 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 @@ -8,7 +8,6 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import ru.otus.coroutineshomework.databinding.FragmentTimerBinding import java.util.Locale @@ -20,7 +19,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 +74,16 @@ class TimerFragment : Fragment() { } private fun startTimer() { - // TODO: Start timer + job = viewLifecycleOwner.lifecycleScope.launch { + while (true){ + delay(1) + time += 1.milliseconds + } + } } private fun stopTimer() { - // TODO: Stop timer + job?.cancel() } override fun onDestroyView() { @@ -94,9 +98,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().mod(60), + this.inWholeSeconds.toInt().mod(60), + this.inWholeMilliseconds.toInt().mod(1000) ) } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 866702f..d444fe8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" appcompat = "1.7.0" +kotlinxTime = "0.5.0" material = "1.12.0" constraintlayout = "2.2.0" lifecycleLivedataKtx = "2.8.7" @@ -21,6 +22,7 @@ junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +kotlinx-time = { module = "org.jetbrains.kotlin:kotlinx-time", version.ref = "kotlinxTime" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" } From e38d71946cc5cb1734e74fb2942eac98ea1ef3e2 Mon Sep 17 00:00:00 2001 From: Huawei Date: Sat, 7 Jun 2025 14:11:35 +0300 Subject: [PATCH 2/5] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=201.2=20-=20=D0=9F=D0=B5=D1=80=D0=B5=D0=B2=D0=BE=D0=B4=20?= =?UTF-8?q?=D1=82=D0=B0=D0=B9=D0=BC=D0=B5=D1=80=D0=B0=20=D0=BD=D0=B0=20Kot?= =?UTF-8?q?lin=20Flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/timer/TimerFragment.kt | 23 +++++++++++++------ 1 file changed, 16 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 b90004f..db91557 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,9 +5,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import ru.otus.coroutineshomework.databinding.FragmentTimerBinding import java.util.Locale @@ -20,9 +25,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() - } + private val timeFlow = MutableStateFlow(Duration.ZERO) private var started by Delegates.observable(false) { _, _, newValue -> setButtonsState(newValue) @@ -52,12 +55,18 @@ class TimerFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) savedInstanceState?.let { - time = it.getLong(TIME).milliseconds + timeFlow.value = it.getLong(TIME).milliseconds started = it.getBoolean(STARTED) } setButtonsState(started) with(binding) { - time.text = this@TimerFragment.time.toDisplayString() + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + timeFlow.collect { + time.text = it.toDisplayString() + } + } + } btnStart.setOnClickListener { started = true } @@ -69,7 +78,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) } @@ -77,7 +86,7 @@ class TimerFragment : Fragment() { job = viewLifecycleOwner.lifecycleScope.launch { while (true){ delay(1) - time += 1.milliseconds + timeFlow.emit(timeFlow.value + 1.milliseconds) } } } From 350914bb71a3ba09821013d30b2b6f0616424482 Mon Sep 17 00:00:00 2001 From: Huawei Date: Sun, 8 Jun 2025 14:26:34 +0300 Subject: [PATCH 3/5] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=202.1=20-=20Login?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/login/LoginViewModel.kt | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 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..2d319e8 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 @@ -3,25 +3,40 @@ package ru.otus.coroutineshomework.ui.login 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 class LoginViewModel : ViewModel() { private val _state = MutableLiveData(LoginViewState.Login()) val state: LiveData = _state + val loginApi = LoginApi() - /** - * Login to the network - * @param name user name - * @param password user password - */ fun login(name: String, password: String) { - // TODO: Implement login + _state.value = LoginViewState.LoggingIn + viewModelScope.launch { + val credentials = Credentials(name, password) + try { + val user = withContext(Dispatchers.IO) { + loginApi.login(credentials) + } + _state.value = LoginViewState.Content(user) + } catch (e: Exception){ + _state.value = LoginViewState.Login(e) + } + } } - /** - * Logout from the network - */ fun logout() { - // TODO: Implement logout + _state.value = LoginViewState.LoggingOut + viewModelScope.launch { + withContext(Dispatchers.IO) { + LoginApi().logout() + } + _state.value = LoginViewState.Login() + } } } From 10c2976f91c2db2fca6481e54fc681727fb6fbb7 Mon Sep 17 00:00:00 2001 From: Huawei Date: Sun, 8 Jun 2025 18:49:56 +0300 Subject: [PATCH 4/5] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=202.2=20-=20Login=20=D1=81=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=D0=BC=20Kotlin?= =?UTF-8?q?=20Flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/login/LoginFragment.kt | 16 +++--- .../ui/login/LoginViewModel.kt | 49 ++++++++++++------- 2 files changed, 42 insertions(+), 23 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..3d4e116 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.stateFlow.collect { viewState -> + when(viewState) { + is LoginViewState.Login -> showLogin(viewState) + LoginViewState.LoggingIn -> showLoggingIn() + is LoginViewState.Content -> showContent(viewState) + 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 2d319e8..21129b1 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 @@ -3,40 +3,55 @@ package ru.otus.coroutineshomework.ui.login import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData 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.asStateFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ru.otus.coroutineshomework.ui.login.data.Credentials class LoginViewModel : ViewModel() { - private val _state = MutableLiveData(LoginViewState.Login()) - val state: LiveData = _state + private val _stateFlow = MutableStateFlow(LoginViewState.Login()) + val stateFlow: StateFlow = _stateFlow.asStateFlow() val loginApi = LoginApi() - fun login(name: String, password: String) { - _state.value = LoginViewState.LoggingIn + fun login(name: String, password: String){ viewModelScope.launch { - val credentials = Credentials(name, password) - try { - val user = withContext(Dispatchers.IO) { - loginApi.login(credentials) - } - _state.value = LoginViewState.Content(user) - } catch (e: Exception){ - _state.value = LoginViewState.Login(e) + loginFlow(name, password).collect { + _stateFlow.emit(it) } } } - fun logout() { - _state.value = LoginViewState.LoggingOut + fun logout(){ viewModelScope.launch { - withContext(Dispatchers.IO) { - LoginApi().logout() + logoutFlow().collect { + _stateFlow.emit(it) } - _state.value = LoginViewState.Login() } } + + fun loginFlow(name: String, password: String) = flow { + emit(LoginViewState.LoggingIn) + val credentials = Credentials(name, password) + try { + val user = loginApi.login(credentials) + emit(LoginViewState.Content(user)) + } catch (e: Exception){ + emit(LoginViewState.Login(e)) + } + }.flowOn(Dispatchers.IO) + + fun logoutFlow() = flow { + emit(LoginViewState.LoggingOut) + loginApi.logout() + emit(LoginViewState.Login()) + }.flowOn(Dispatchers.IO) } From 8c7bcedd2313fdc47676c76aff3c5c997f305633 Mon Sep 17 00:00:00 2001 From: Huawei Date: Sun, 8 Jun 2025 20:08:24 +0300 Subject: [PATCH 5/5] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=203.=20Speed-test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/network/NetworkViewModel.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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..c318efa 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,11 @@ 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.async import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.random.Random @@ -18,7 +21,21 @@ class NetworkViewModel : ViewModel() { val result: LiveData = _result fun startTest(numberOfThreads: Int) { - // TODO: Implement the logic + viewModelScope.launch { + _running.value = true + _result.value = null + val jobs = List(numberOfThreads) { + async { emulateBlockingNetworkRequest() } + } + val successfulTimeList = jobs.mapNotNull { it.await().getOrNull() } + val averageTime = if (successfulTimeList.isNotEmpty()) { + successfulTimeList.average().toLong() + } else { + 0L + } + _result.value = averageTime + _running.value = false + } } private companion object {