From c86581bb62db5e03198724a04c6bbb742864fd28 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Sun, 23 Nov 2025 21:18:38 +0300 Subject: [PATCH 01/10] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=201.1=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/timer/TimerFragment.kt | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 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..55ebcfc 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 @@ -15,12 +15,16 @@ import java.util.Locale import kotlin.properties.Delegates import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds class TimerFragment : Fragment() { private var _binding: FragmentTimerBinding? = null private val binding get() = _binding!! + private var timerJob: Job? = null + private var time: Duration by Delegates.observable(Duration.ZERO) { _, _, newValue -> binding.time.text = newValue.toDisplayString() } @@ -75,11 +79,17 @@ class TimerFragment : Fragment() { } private fun startTimer() { - // TODO: Start timer + timerJob = lifecycleScope.launch { + while (isActive) { + delay(TIMER_DELAY_MS) // Обновление примерно 60 раз в секунду (~16.67 мс) + time += timerDelayMs + } + } } private fun stopTimer() { - // TODO: Stop timer + timerJob?.cancel() + timerJob = null } override fun onDestroyView() { @@ -91,12 +101,19 @@ class TimerFragment : Fragment() { private const val TIME = "time" private const val STARTED = "started" + private const val TIMER_DELAY_MS = 16L + + private val timerDelayMs = TIMER_DELAY_MS.milliseconds + + private val secondInMinute = 1.minutes.inWholeSeconds + private val millisecondInSecond = 1.seconds.inWholeMilliseconds + private fun Duration.toDisplayString(): String = String.format( Locale.getDefault(), "%02d:%02d.%03d", - this.inWholeMinutes.toInt(), - this.inWholeSeconds.toInt(), - this.inWholeMilliseconds.toInt() + inWholeMinutes.toInt(), + (inWholeSeconds % secondInMinute).toInt(), + (inWholeMilliseconds % millisecondInSecond).toInt() ) } } \ No newline at end of file From 8e74972300f52d78cca8ef63f530ff0daf7d1be6 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Sun, 23 Nov 2025 21:47:26 +0300 Subject: [PATCH 02/10] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=202.1=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/login/LoginViewModel.kt | 25 +++++++++++++++++-- 1 file changed, 23 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..72a665f 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,6 +3,10 @@ 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 ru.otus.coroutineshomework.ui.login.data.Credentials class LoginViewModel : ViewModel() { @@ -15,13 +19,30 @@ class LoginViewModel : ViewModel() { * @param password user password */ fun login(name: String, password: String) { - // TODO: Implement login + _state.postValue(LoginViewState.LoggingIn) + viewModelScope.launch(Dispatchers.IO) { + runCatching { + val user = LoginApi().login(Credentials(name, password)) + _state.postValue(LoginViewState.Content(user)) + }.onFailure { + _state.postValue(LoginViewState.Login(it as? Exception)) + } + + } } /** * Logout from the network */ fun logout() { - // TODO: Implement logout + _state.postValue(LoginViewState.LoggingOut) + viewModelScope.launch(Dispatchers.IO) { + runCatching { + LoginApi().logout() + _state.postValue(LoginViewState.Login()) + }.onFailure { + _state.postValue(LoginViewState.Login(it as? Exception)) + } + } } } From 29ccfafa010d32d432f12a971b31a2bd161292e7 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Sun, 23 Nov 2025 22:21:27 +0300 Subject: [PATCH 03/10] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=203=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/network/NetworkViewModel.kt | 17 ++++++++++++++++- 1 file changed, 16 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..1ec79de 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.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.random.Random @@ -18,7 +22,18 @@ class NetworkViewModel : ViewModel() { val result: LiveData = _result fun startTest(numberOfThreads: Int) { - // TODO: Implement the logic + _running.postValue(true) + viewModelScope.launch { + // по умолчанию: в Main Dispatcher + val avg = (1..numberOfThreads) + .map { async { emulateBlockingNetworkRequest() } } // переход в IO Dispatcher + .awaitAll() + .filter { it.isSuccess } + .mapNotNull { it.getOrNull() } + .average() + _result.postValue(avg.toLong()) + _running.postValue(false) + } } private companion object { From f9c6b5da24154b04105dbf8c4304b3eaf371768a Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Wed, 26 Nov 2025 16:53:04 +0300 Subject: [PATCH 04/10] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=201.2=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/timer/TimerFragment.kt | 27 ++++++++++++++----- 1 file changed, 20 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 55ebcfc..269d399 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.MutableStateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import ru.otus.coroutineshomework.databinding.FragmentTimerBinding @@ -25,9 +28,11 @@ class TimerFragment : Fragment() { private var timerJob: Job? = null - private var time: Duration by Delegates.observable(Duration.ZERO) { _, _, newValue -> - binding.time.text = newValue.toDisplayString() - } +// private var time: Duration by Delegates.observable(Duration.ZERO) { _, _, newValue -> +// binding.time.text = newValue.toDisplayString() +// } + + private var timeFlow: MutableStateFlow = MutableStateFlow(Duration.ZERO) private var started by Delegates.observable(false) { _, _, newValue -> setButtonsState(newValue) @@ -57,12 +62,20 @@ 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) + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED){ + timeFlow.collect { time -> + binding.time.text = time.toDisplayString() + } + } + } + with(binding) { - time.text = this@TimerFragment.time.toDisplayString() btnStart.setOnClickListener { started = true } @@ -74,7 +87,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) } @@ -82,7 +95,7 @@ class TimerFragment : Fragment() { timerJob = lifecycleScope.launch { while (isActive) { delay(TIMER_DELAY_MS) // Обновление примерно 60 раз в секунду (~16.67 мс) - time += timerDelayMs + timeFlow.emit(timeFlow.value + timerDelayMs) } } } From 961468a3609a4fa62bca4b174b64d17c57c1e5cb Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Sat, 29 Nov 2025 23:16:30 +0300 Subject: [PATCH 05/10] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=202.2=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/login/LoginFragment.kt | 10 ++-- .../ui/login/LoginViewModel.kt | 47 +++++++++++-------- 2 files changed, 34 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..83705e3 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,9 @@ 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.flow.launchIn +import kotlinx.coroutines.flow.onEach import ru.otus.coroutineshomework.databinding.ContentBinding import ru.otus.coroutineshomework.databinding.FragmentLoginBinding import ru.otus.coroutineshomework.databinding.LoadingBinding @@ -42,15 +45,14 @@ class LoginFragment : Fragment() { setupLogin() setupContent() - - loginViewModel.state.observe(viewLifecycleOwner) { - when(it) { + loginViewModel.state.onEach { + when (it) { is LoginViewState.Login -> showLogin(it) LoginViewState.LoggingIn -> showLoggingIn() is LoginViewState.Content -> showContent(it) LoginViewState.LoggingOut -> showLoggingOut() } - } + }.launchIn(lifecycleScope) } private fun setupLogin() { 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 72a665f..12e358f 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,17 +1,19 @@ 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.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch 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.asStateFlow() /** * Login to the network @@ -19,15 +21,8 @@ class LoginViewModel : ViewModel() { * @param password user password */ fun login(name: String, password: String) { - _state.postValue(LoginViewState.LoggingIn) viewModelScope.launch(Dispatchers.IO) { - runCatching { - val user = LoginApi().login(Credentials(name, password)) - _state.postValue(LoginViewState.Content(user)) - }.onFailure { - _state.postValue(LoginViewState.Login(it as? Exception)) - } - + loginFlow(name, password).collect { _stateFlow.emit(it) } } } @@ -35,14 +30,28 @@ class LoginViewModel : ViewModel() { * Logout from the network */ fun logout() { - _state.postValue(LoginViewState.LoggingOut) viewModelScope.launch(Dispatchers.IO) { - runCatching { - LoginApi().logout() - _state.postValue(LoginViewState.Login()) - }.onFailure { - _state.postValue(LoginViewState.Login(it as? Exception)) - } + logoutFlow().collect { _stateFlow.emit(it) } + } + } + + private fun loginFlow(name: String, password: String): Flow = flow { + emit(LoginViewState.LoggingIn) + runCatching { + val user = LoginApi().login(Credentials(name, password)) + emit(LoginViewState.Content(user)) + }.onFailure { + emit(LoginViewState.Login(it as? Exception)) + } + } + + private fun logoutFlow(): Flow = flow { + emit(LoginViewState.LoggingOut) + runCatching { + LoginApi().logout() + emit(LoginViewState.Login()) + }.onFailure { + emit(LoginViewState.Login(it as? Exception)) } } } From ceea77857f6c068d5ee3e0585928fdc5ab5283f7 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Sat, 29 Nov 2025 23:27:56 +0300 Subject: [PATCH 06/10] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=202.2=20loginApi=20=D0=BE=D0=B1=D1=8A=D1=8F=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D1=82=D1=81=D1=8F=20=D0=BF=D1=80=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B8=20=D0=BA=D0=BB=D0=B0?= =?UTF-8?q?=D1=81=D1=81=D0=B0,=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=D0=B4=D1=83=D1=8E=D1=89=D0=B5=D0=B3=D0=BE=20=D0=B2?= =?UTF-8?q?=D0=BD=D0=B5=D0=B4=D1=80=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=BA=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ru/otus/coroutineshomework/ui/login/LoginViewModel.kt | 6 ++++-- 1 file changed, 4 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 12e358f..e824ffb 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 @@ -12,6 +12,8 @@ import ru.otus.coroutineshomework.ui.login.data.Credentials class LoginViewModel : ViewModel() { + private val loginApi = LoginApi() + private val _stateFlow = MutableStateFlow(LoginViewState.Login()) val state = _stateFlow.asStateFlow() @@ -38,7 +40,7 @@ class LoginViewModel : ViewModel() { private fun loginFlow(name: String, password: String): Flow = flow { emit(LoginViewState.LoggingIn) runCatching { - val user = LoginApi().login(Credentials(name, password)) + val user = loginApi.login(Credentials(name, password)) emit(LoginViewState.Content(user)) }.onFailure { emit(LoginViewState.Login(it as? Exception)) @@ -48,7 +50,7 @@ class LoginViewModel : ViewModel() { private fun logoutFlow(): Flow = flow { emit(LoginViewState.LoggingOut) runCatching { - LoginApi().logout() + loginApi.logout() emit(LoginViewState.Login()) }.onFailure { emit(LoginViewState.Login(it as? Exception)) From cae2eb7ac0749ea6ab991dffe229eaa295b5fb6e Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Wed, 3 Dec 2025 20:35:03 +0300 Subject: [PATCH 07/10] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B8=20=D0=B4=D0=BB=D1=8F=20hilt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 13 +++++++-- build.gradle.kts | 3 ++ gradle/libs.versions.toml | 35 ++++++++++++++---------- gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f6d9019..86cdc45 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,16 +1,18 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + alias(libs.plugins.hilt.android) + alias(libs.plugins.ksp) } android { namespace = "ru.otus.coroutineshomework" - compileSdk = 35 + compileSdk = 36 defaultConfig { applicationId = "ru.otus.coroutineshomework" minSdk = 26 - targetSdk = 35 + targetSdk = 36 versionCode = 1 versionName = "1.0" @@ -20,7 +22,10 @@ android { buildTypes { release { isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) } } compileOptions { @@ -48,4 +53,6 @@ dependencies { implementation(libs.androidx.navigation.fragment.ktx) implementation(libs.androidx.navigation.ui.ktx) implementation(libs.kotlinx.coroutines.android) + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) } \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 922f551..4059d3e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,8 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.android) apply false + alias (libs.plugins.hilt.android) apply false + alias (libs.plugins.ksp) apply false } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 866702f..140ae94 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,19 +1,21 @@ [versions] -agp = "8.7.3" -kotlin = "2.1.0" -coreKtx = "1.15.0" +agp = "8.13.1" +kotlin = "2.2.21" +coreKtx = "1.17.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" -coroutines = "1.9.0" -desugar = "1.1.5" +junitVersion = "1.3.0" +espressoCore = "3.7.0" +appcompat = "1.7.1" +material = "1.13.0" +constraintlayout = "2.2.1" +lifecycleLivedataKtx = "2.10.0" +lifecycleViewmodelKtx = "2.10.0" +navigationFragmentKtx = "2.9.6" +navigationUiKtx = "2.9.6" +coroutines = "1.10.2" +desugar = "2.1.5" +hilt = "2.57.2" +ksp = "2.2.21-2.0.4" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -29,8 +31,13 @@ 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" } +hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f469e3b..a34625a 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.13-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 45beb951fd049b5dc9fb6ff21d4ada4173b96007 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Wed, 3 Dec 2025 20:37:16 +0300 Subject: [PATCH 08/10] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=202.2=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=20Hilt=20=D0=B4=D0=BB=D1=8F=20LoginApi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 3 ++- app/src/main/kotlin/ru/otus/coroutineshomework/App.kt | 7 +++++++ .../main/kotlin/ru/otus/coroutineshomework/MainActivity.kt | 5 +++-- .../kotlin/ru/otus/coroutineshomework/ui/login/LoginApi.kt | 7 +++++-- .../ru/otus/coroutineshomework/ui/login/LoginFragment.kt | 2 ++ .../ru/otus/coroutineshomework/ui/login/LoginViewModel.kt | 7 ++++--- 6 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 app/src/main/kotlin/ru/otus/coroutineshomework/App.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 88913ed..635face 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,7 +11,8 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.CoroutinesHomework" - tools:targetApi="31"> + tools:targetApi="31" + android:name=".App"> emulateNetworkRequest(crossinline block: () -> T): T { Log.i(TAG, "emulateNetworkRequest: running on ${Thread.currentThread().name}") - if(Looper.getMainLooper().thread == Thread.currentThread()) { + if (Looper.getMainLooper().thread == Thread.currentThread()) { throw NetworkOnMainThreadException() } Thread.sleep(NETWORK_DELAY) 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 83705e3..6e33008 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 @@ -8,6 +8,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import ru.otus.coroutineshomework.databinding.ContentBinding @@ -15,6 +16,7 @@ import ru.otus.coroutineshomework.databinding.FragmentLoginBinding import ru.otus.coroutineshomework.databinding.LoadingBinding import ru.otus.coroutineshomework.databinding.LoginBinding +@AndroidEntryPoint class LoginFragment : Fragment() { private var _binding: FragmentLoginBinding? = null 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 e824ffb..d66cdcb 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 @@ -2,6 +2,8 @@ package ru.otus.coroutineshomework.ui.login import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import jakarta.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -10,9 +12,8 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import ru.otus.coroutineshomework.ui.login.data.Credentials -class LoginViewModel : ViewModel() { - - private val loginApi = LoginApi() +@HiltViewModel +class LoginViewModel @Inject constructor(private val loginApi: LoginApi) : ViewModel() { private val _stateFlow = MutableStateFlow(LoginViewState.Login()) val state = _stateFlow.asStateFlow() From 8a049600560c54c2bde39d575782331b30090231 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Wed, 3 Dec 2025 21:23:06 +0300 Subject: [PATCH 09/10] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=201.2=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=20Hilt=20=D0=B4=D0=BB=D1=8F=20=D1=82=D0=B0?= =?UTF-8?q?=D0=B9=D0=BC=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/timer/TimerFragment.kt | 45 ++++-------------- .../ui/timer/TimerViewModel.kt | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+), 37 deletions(-) create mode 100644 app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerViewModel.kt 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 269d399..1b47023 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,34 +5,27 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup 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.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.isActive +import dagger.hilt.android.AndroidEntryPoint 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.milliseconds import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +@AndroidEntryPoint class TimerFragment : Fragment() { private var _binding: FragmentTimerBinding? = null private val binding get() = _binding!! - private var timerJob: Job? = null + private val timerViewModel by viewModels() -// private var time: Duration by Delegates.observable(Duration.ZERO) { _, _, newValue -> -// binding.time.text = newValue.toDisplayString() -// } - - private var timeFlow: MutableStateFlow = MutableStateFlow(Duration.ZERO) private var started by Delegates.observable(false) { _, _, newValue -> setButtonsState(newValue) @@ -61,15 +54,11 @@ class TimerFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - savedInstanceState?.let { - timeFlow.value = it.getLong(TIME).milliseconds - started = it.getBoolean(STARTED) - } setButtonsState(started) viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED){ - timeFlow.collect { time -> + repeatOnLifecycle(Lifecycle.State.STARTED) { + timerViewModel.timeFlow.collect { time -> binding.time.text = time.toDisplayString() } } @@ -85,24 +74,12 @@ class TimerFragment : Fragment() { } } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putLong(TIME, timeFlow.value.inWholeMilliseconds) - outState.putBoolean(STARTED, started) - } - private fun startTimer() { - timerJob = lifecycleScope.launch { - while (isActive) { - delay(TIMER_DELAY_MS) // Обновление примерно 60 раз в секунду (~16.67 мс) - timeFlow.emit(timeFlow.value + timerDelayMs) - } - } + timerViewModel.startTimer() } private fun stopTimer() { - timerJob?.cancel() - timerJob = null + timerViewModel.stopTimer() } override fun onDestroyView() { @@ -111,12 +88,6 @@ class TimerFragment : Fragment() { } companion object { - private const val TIME = "time" - private const val STARTED = "started" - - private const val TIMER_DELAY_MS = 16L - - private val timerDelayMs = TIMER_DELAY_MS.milliseconds private val secondInMinute = 1.minutes.inWholeSeconds private val millisecondInSecond = 1.seconds.inWholeMilliseconds diff --git a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerViewModel.kt b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerViewModel.kt new file mode 100644 index 0000000..3e4bb16 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerViewModel.kt @@ -0,0 +1,47 @@ +package ru.otus.coroutineshomework.ui.timer + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.scopes.ViewModelScoped +import jakarta.inject.Inject +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +@ViewModelScoped +class TimerViewModel @Inject constructor() : ViewModel() { + + private val _timeFlow: MutableStateFlow = MutableStateFlow(Duration.ZERO) + val timeFlow = _timeFlow.asStateFlow() + + private var timerJob: Job? = null + + fun startTimer() { + timerJob = viewModelScope.launch { + while (isActive) { + delay(TIMER_DELAY_MS) // Обновление примерно 60 раз в секунду (~16.67 мс) + _timeFlow.emit(timeFlow.value + timerDelayMs) + } + } + } + + fun stopTimer() { + timerJob?.cancel() + timerJob = null + } + + companion object { + + private const val TIMER_DELAY_MS = 16L + + private val timerDelayMs = TIMER_DELAY_MS.milliseconds + + + } + +} \ No newline at end of file From 7f742c6630b8fa2bfc092623f09cf9f9f974debb Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Wed, 3 Dec 2025 22:24:11 +0300 Subject: [PATCH 10/10] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=201.2=20=D1=82=D0=B0=D0=B9=D0=BC=D0=B5=D1=80=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=20=D0=B8=D0=B7=20ViewModel?= =?UTF-8?q?=20=D0=B8=20=D0=B2=D0=BD=D0=B5=D0=B4=D1=80=D0=B5=D0=BD=20=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B7=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/timer/TimerFragment.kt | 23 +++++++++---- .../{TimerViewModel.kt => TimerUsecase.kt} | 33 +++++++++++++++---- 2 files changed, 43 insertions(+), 13 deletions(-) rename app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/{TimerViewModel.kt => TimerUsecase.kt} (59%) 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 1b47023..5c5d67b 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 @@ -1,15 +1,16 @@ package ru.otus.coroutineshomework.ui.timer import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import dagger.hilt.android.AndroidEntryPoint +import jakarta.inject.Inject import kotlinx.coroutines.launch import ru.otus.coroutineshomework.databinding.FragmentTimerBinding import java.util.Locale @@ -24,7 +25,8 @@ class TimerFragment : Fragment() { private var _binding: FragmentTimerBinding? = null private val binding get() = _binding!! - private val timerViewModel by viewModels() + @set:Inject + lateinit var timerUseCase: TimerUseCase private var started by Delegates.observable(false) { _, _, newValue -> @@ -54,11 +56,15 @@ class TimerFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + savedInstanceState?.let { + started = it.getBoolean(STARTED) + } setButtonsState(started) viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - timerViewModel.timeFlow.collect { time -> + timerUseCase.timeFlow.collect { time -> + Log.d("TimerFragment", "Time updated: ${time.toDisplayString()}") binding.time.text = time.toDisplayString() } } @@ -74,12 +80,17 @@ class TimerFragment : Fragment() { } } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(STARTED, started) + } + private fun startTimer() { - timerViewModel.startTimer() + timerUseCase.startTimer() } private fun stopTimer() { - timerViewModel.stopTimer() + timerUseCase.stopTimer() } override fun onDestroyView() { @@ -88,7 +99,7 @@ class TimerFragment : Fragment() { } companion object { - + private const val STARTED = "started" private val secondInMinute = 1.minutes.inWholeSeconds private val millisecondInSecond = 1.seconds.inWholeMilliseconds diff --git a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerViewModel.kt b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerUsecase.kt similarity index 59% rename from app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerViewModel.kt rename to app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerUsecase.kt index 3e4bb16..e33353f 100644 --- a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerViewModel.kt +++ b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerUsecase.kt @@ -1,20 +1,27 @@ package ru.otus.coroutineshomework.ui.timer -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.scopes.ViewModelScoped -import jakarta.inject.Inject +import android.util.Log +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import javax.inject.Singleton +import kotlin.coroutines.CoroutineContext import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds -@ViewModelScoped -class TimerViewModel @Inject constructor() : ViewModel() { + +class TimerUseCase : CoroutineScope { + + override val coroutineContext: CoroutineContext = SupervisorJob() private val _timeFlow: MutableStateFlow = MutableStateFlow(Duration.ZERO) val timeFlow = _timeFlow.asStateFlow() @@ -22,9 +29,11 @@ class TimerViewModel @Inject constructor() : ViewModel() { private var timerJob: Job? = null fun startTimer() { - timerJob = viewModelScope.launch { + if (timerJob != null) return + timerJob = launch { while (isActive) { delay(TIMER_DELAY_MS) // Обновление примерно 60 раз в секунду (~16.67 мс) + Log.i("TimerUseCase", "Timer updated ${timeFlow.value + timerDelayMs}") _timeFlow.emit(timeFlow.value + timerDelayMs) } } @@ -44,4 +53,14 @@ class TimerViewModel @Inject constructor() : ViewModel() { } +} + +@Module +@InstallIn(SingletonComponent::class) +class AppModule { + + @Provides + @Singleton + fun timerUseCase() = TimerUseCase() + } \ No newline at end of file