From 9610e3fe49b16d7b12a0372f062c97aaaff4f563 Mon Sep 17 00:00:00 2001 From: Evgenii Balandin Date: Mon, 27 Jan 2025 23:39:34 +0300 Subject: [PATCH 01/10] Task 1.1 Timer --- .../coroutineshomework/ui/timer/TimerFragment.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 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..6ba0b5e 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 @@ -14,6 +14,7 @@ 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() { @@ -34,6 +35,8 @@ class TimerFragment : Fragment() { } } + private var timerJob: Job? = null + private fun setButtonsState(started: Boolean) { with(binding) { btnStart.isEnabled = !started @@ -75,11 +78,20 @@ class TimerFragment : Fragment() { } private fun startTimer() { - // TODO: Start timer + timerJob?.cancel() // Остановить предыдущую корутину, если она запущена + + timerJob = viewLifecycleOwner.lifecycleScope.launch { + time = ZERO + while (isActive) { // Проверяем, что корутина активна + delay(10) // Интервал обновления (например, 10 миллисекунд) + time += 10.milliseconds // Увеличиваем значение времени + } + } } private fun stopTimer() { - // TODO: Stop timer + timerJob?.cancel() // Останавливаем корутину + timerJob = null } override fun onDestroyView() { From 169efe51b2c14ee7c3c6a3fdbf1440e68a0f0e8d Mon Sep 17 00:00:00 2001 From: Evgenii Balandin Date: Mon, 27 Jan 2025 23:40:36 +0300 Subject: [PATCH 02/10] Task 1.1 Timer clear --- .../kotlin/ru/otus/coroutineshomework/ui/timer/TimerFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6ba0b5e..f66fc4d 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 @@ -22,7 +22,7 @@ class TimerFragment : Fragment() { private var _binding: FragmentTimerBinding? = null private val binding get() = _binding!! - private var time: Duration by Delegates.observable(Duration.ZERO) { _, _, newValue -> + private var time: Duration by Delegates.observable(ZERO) { _, _, newValue -> binding.time.text = newValue.toDisplayString() } From a3d706e56f817834b2721b6a13b5cbeda46058b4 Mon Sep 17 00:00:00 2001 From: Evgenii Balandin Date: Tue, 28 Jan 2025 01:37:08 +0300 Subject: [PATCH 03/10] Task 1.2 Timer By SharedFlow --- .../ui/timer/TimerFragment.kt | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 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 f66fc4d..9047325 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,12 @@ 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.MutableSharedFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import ru.otus.coroutineshomework.databinding.FragmentTimerBinding @@ -22,9 +25,10 @@ class TimerFragment : Fragment() { private var _binding: FragmentTimerBinding? = null private val binding get() = _binding!! - private var time: Duration by Delegates.observable(ZERO) { _, _, newValue -> - binding.time.text = newValue.toDisplayString() - } + private var timeFlow: MutableSharedFlow = MutableSharedFlow(replay = 1) + private var timerJob: Job? = null + private val time: Duration get() = timeFlow.replayCache.firstOrNull() ?: ZERO + private var started by Delegates.observable(false) { _, _, newValue -> setButtonsState(newValue) @@ -35,7 +39,6 @@ class TimerFragment : Fragment() { } } - private var timerJob: Job? = null private fun setButtonsState(started: Boolean) { with(binding) { @@ -55,13 +58,22 @@ class TimerFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // Подписка на timeFlow для обновления UI + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + timeFlow.collect { duration -> + binding.time.text = duration.toDisplayString() + } + } + } + // Восстанавливаем значение времени из Bundle или используем значение по умолчанию + val initialTime = savedInstanceState?.getLong(TIME)?.milliseconds ?: ZERO + timeFlow.tryEmit(initialTime) savedInstanceState?.let { - time = it.getLong(TIME).milliseconds started = it.getBoolean(STARTED) } setButtonsState(started) with(binding) { - time.text = this@TimerFragment.time.toDisplayString() btnStart.setOnClickListener { started = true } @@ -79,12 +91,13 @@ class TimerFragment : Fragment() { private fun startTimer() { timerJob?.cancel() // Остановить предыдущую корутину, если она запущена - + var currentTime = time timerJob = viewLifecycleOwner.lifecycleScope.launch { - time = ZERO while (isActive) { // Проверяем, что корутина активна - delay(10) // Интервал обновления (например, 10 миллисекунд) - time += 10.milliseconds // Увеличиваем значение времени + val delta = 10L; + delay(delta) // Интервал обновления (например, 10 миллисекунд) + currentTime += delta.milliseconds + timeFlow.emit(currentTime) // Обновляем значение через emit } } } @@ -107,8 +120,8 @@ class TimerFragment : Fragment() { Locale.getDefault(), "%02d:%02d.%03d", this.inWholeMinutes.toInt(), - this.inWholeSeconds.toInt(), - this.inWholeMilliseconds.toInt() + this.inWholeSeconds.toInt() % 60, + this.inWholeMilliseconds.toInt() % 1000 ) } } \ No newline at end of file From 21d5beacaf9b38702db1b43128196d195e85fe4a Mon Sep 17 00:00:00 2001 From: Evgenii Balandin Date: Tue, 28 Jan 2025 01:37:50 +0300 Subject: [PATCH 04/10] Task 1.2 Timer By SharedFlow clearance --- .../kotlin/ru/otus/coroutineshomework/ui/timer/TimerFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9047325..ecf0c27 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 @@ -94,7 +94,7 @@ class TimerFragment : Fragment() { var currentTime = time timerJob = viewLifecycleOwner.lifecycleScope.launch { while (isActive) { // Проверяем, что корутина активна - val delta = 10L; + val delta = 10L delay(delta) // Интервал обновления (например, 10 миллисекунд) currentTime += delta.milliseconds timeFlow.emit(currentTime) // Обновляем значение через emit From 60c2118cb666f7d6a9195cc6aed4794d86c86138 Mon Sep 17 00:00:00 2001 From: Evgenii Balandin Date: Tue, 28 Jan 2025 02:46:51 +0300 Subject: [PATCH 05/10] Task 2.1 - Login LiveData --- .../ui/login/LoginViewModel.kt | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 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..7d8c548 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,11 +3,17 @@ 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 + private val loginApi = LoginApi() // Экземпляр LoginApi для выполнения операций /** * Login to the network @@ -15,13 +21,40 @@ class LoginViewModel : ViewModel() { * @param password user password */ fun login(name: String, password: String) { - // TODO: Implement login + viewModelScope.launch { + _state.value = LoginViewState.LoggingIn // Устанавливаем состояние загрузки + + try { + val user = withContext(Dispatchers.IO) { + loginApi.login(Credentials(name, password)) // Выполняем сетевой запрос + } + _state.value = LoginViewState.Content(user) // Успешный вход + } catch (e: Exception) { + _state.value = LoginViewState.Login(e) // Ошибка входа + } + } } /** * Logout from the network */ fun logout() { - // TODO: Implement logout + viewModelScope.launch { + val currentState = _state.value + _state.value = LoginViewState.LoggingOut // Устанавливаем состояние загрузки + + try { + withContext(Dispatchers.IO) { + loginApi.logout() // Выполняем сетевой запрос + } + _state.value = LoginViewState.Login() // Успешный выход + } catch (e: Exception) { + // В случае ошибки выхода возвращаемся к предыдущему состоянию + _state.value = when (currentState) { + is LoginViewState.Content -> currentState // Если пользователь был залогинен, оставляем Content + else -> LoginViewState.Login(e) // Если нет, возвращаемся в Login с ошибкой + } + } + } } } From ddb79aefa0c765db90c276efe74fbe66bd8c0b6b Mon Sep 17 00:00:00 2001 From: Evgenii Balandin Date: Tue, 28 Jan 2025 03:04:21 +0300 Subject: [PATCH 06/10] Task 2.2 - Login Flow --- .../ui/login/LoginFragment.kt | 22 +++++++--- .../ui/login/LoginViewModel.kt | 43 ++++++++++++------- 2 files changed, 44 insertions(+), 21 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..d524c60 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,10 @@ import android.view.ViewGroup import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.launch import ru.otus.coroutineshomework.databinding.ContentBinding import ru.otus.coroutineshomework.databinding.FragmentLoginBinding import ru.otus.coroutineshomework.databinding.LoadingBinding @@ -43,12 +47,18 @@ 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() + // Подписка на состояние через collect + viewLifecycleOwner.lifecycleScope.launch { + // repeatOnLifecycle автоматически отслеживает жизненный цикл фрагмента + viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + 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 7d8c548..b732727 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 @@ -5,14 +5,17 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow 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 state: StateFlow = _stateFlow // Экспортируем как StateFlow + private val loginApi = LoginApi() // Экземпляр LoginApi для выполнения операций /** @@ -22,15 +25,20 @@ class LoginViewModel : ViewModel() { */ fun login(name: String, password: String) { viewModelScope.launch { - _state.value = LoginViewState.LoggingIn // Устанавливаем состояние загрузки + // Сначала устанавливаем состояние "вход в процесс" + _stateFlow.emit(LoginViewState.LoggingIn) try { + // Выполняем сетевой запрос в фоновом потоке val user = withContext(Dispatchers.IO) { - loginApi.login(Credentials(name, password)) // Выполняем сетевой запрос + loginApi.login(Credentials(name, password)) // Запрос на вход } - _state.value = LoginViewState.Content(user) // Успешный вход + + // Успешный вход + _stateFlow.emit(LoginViewState.Content(user)) } catch (e: Exception) { - _state.value = LoginViewState.Login(e) // Ошибка входа + // Ошибка входа + _stateFlow.emit(LoginViewState.Login(e)) } } } @@ -40,20 +48,25 @@ class LoginViewModel : ViewModel() { */ fun logout() { viewModelScope.launch { - val currentState = _state.value - _state.value = LoginViewState.LoggingOut // Устанавливаем состояние загрузки + _stateFlow.emit(LoginViewState.LoggingOut) // Состояние "выход в процессе" try { + // Выполняем сетевой запрос на выход withContext(Dispatchers.IO) { - loginApi.logout() // Выполняем сетевой запрос + loginApi.logout() // Запрос на выход } - _state.value = LoginViewState.Login() // Успешный выход + + // Успешный выход + _stateFlow.emit(LoginViewState.Login()) // Возвращаемся в начальное состояние } catch (e: Exception) { - // В случае ошибки выхода возвращаемся к предыдущему состоянию - _state.value = when (currentState) { - is LoginViewState.Content -> currentState // Если пользователь был залогинен, оставляем Content - else -> LoginViewState.Login(e) // Если нет, возвращаемся в Login с ошибкой - } + // Ошибка выхода + val currentState = _stateFlow.value + _stateFlow.emit( + when (currentState) { + is LoginViewState.Content -> currentState // Если был залогинен, оставляем Content + else -> LoginViewState.Login(e) // В противном случае — возвращаемся к Login с ошибкой + } + ) } } } From fe5c12f48414746072b1efaf20554c1efc84d4b3 Mon Sep 17 00:00:00 2001 From: Evgenii Balandin Date: Tue, 28 Jan 2025 03:04:52 +0300 Subject: [PATCH 07/10] Task 2.2 - Login Flow clearance --- .../ru/otus/coroutineshomework/ui/login/LoginViewModel.kt | 2 -- 1 file changed, 2 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 b732727..dae66e7 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,7 +1,5 @@ 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 From 301d9c4cdcc5b427e5ee13e9cfbbc91bca151fd4 Mon Sep 17 00:00:00 2001 From: Evgenii Balandin Date: Tue, 28 Jan 2025 03:28:42 +0300 Subject: [PATCH 08/10] Task 3. Speed-test --- .../ui/network/NetworkFragment.kt | 2 +- .../ui/network/NetworkViewModel.kt | 37 ++++++++++++++++++- app/src/main/res/layout/fragment_network.xml | 4 +- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/network/NetworkFragment.kt b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/network/NetworkFragment.kt index 76e411c..4cda1af 100644 --- a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/network/NetworkFragment.kt +++ b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/network/NetworkFragment.kt @@ -36,7 +36,7 @@ class NetworkFragment : Fragment() { } networkViewModel.result.observe(viewLifecycleOwner) { result -> - binding.result.text = result?.let { getString(R.string.result, it.toFloat() / 1000) } ?: "" + binding.result.text = result?.let { getString(R.string.result, it.toFloat() / 1000) } ?: "no successful connections" } binding.numOfThreads.setText( 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..798ef35 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,38 @@ class NetworkViewModel : ViewModel() { val result: LiveData = _result fun startTest(numberOfThreads: Int) { - // TODO: Implement the logic + viewModelScope.launch { + _running.value = true // Устанавливаем индикатор загрузки в true + + val results = mutableListOf() + val jobs = mutableListOf>>() + + // Запускаем несколько корутин + repeat(numberOfThreads) { + val job = async { + emulateBlockingNetworkRequest() + } + jobs.add(job) + } + + // Собираем результаты + jobs.forEach { job -> + val result = job.await() + result.onSuccess { + results.add(it) // Добавляем время успешных запросов + } + } + + // Если есть успешные результаты, вычисляем их среднее время + if (results.isNotEmpty()) { + val averageTime = results.average().toLong() + _result.value = averageTime + } else { + _result.value = null // Если нет успешных запросов + } + + _running.value = false // Останавливаем индикатор загрузки + } } private companion object { diff --git a/app/src/main/res/layout/fragment_network.xml b/app/src/main/res/layout/fragment_network.xml index 9ce4a43..65273da 100644 --- a/app/src/main/res/layout/fragment_network.xml +++ b/app/src/main/res/layout/fragment_network.xml @@ -58,6 +58,8 @@ app:layout_constraintBottom_toTopOf="@+id/numOfThreadsLayout" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + android:gravity="center" + /> \ No newline at end of file From 2dbffa69140304d66113e9e98c010547dea58284 Mon Sep 17 00:00:00 2001 From: Evgenii Balandin Date: Tue, 28 Jan 2025 03:44:51 +0300 Subject: [PATCH 09/10] Task 3. Speed-test clearance --- app/src/main/res/values-night/themes.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index d5ab143..f67862f 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,6 +1,6 @@ -