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/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 06c3afe..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 @@ -7,11 +7,16 @@ import android.view.ViewGroup 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 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 @@ -42,15 +47,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 5fae38a..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 @@ -1,13 +1,22 @@ package ru.otus.coroutineshomework.ui.login -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData 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 +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() { +@HiltViewModel +class LoginViewModel @Inject constructor(private val loginApi: LoginApi) : 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 @@ -15,13 +24,37 @@ class LoginViewModel : ViewModel() { * @param password user password */ fun login(name: String, password: String) { - // TODO: Implement login + viewModelScope.launch(Dispatchers.IO) { + loginFlow(name, password).collect { _stateFlow.emit(it) } + } } /** * Logout from the network */ fun logout() { - // TODO: Implement logout + viewModelScope.launch(Dispatchers.IO) { + 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)) + } } } 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 { 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..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,29 +1,33 @@ 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.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive +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 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 time: Duration by Delegates.observable(Duration.ZERO) { _, _, newValue -> - binding.time.text = newValue.toDisplayString() - } + @set:Inject + lateinit var timerUseCase: TimerUseCase + private var started by Delegates.observable(false) { _, _, newValue -> setButtonsState(newValue) @@ -53,12 +57,20 @@ class TimerFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) savedInstanceState?.let { - time = it.getLong(TIME).milliseconds started = it.getBoolean(STARTED) } setButtonsState(started) + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + timerUseCase.timeFlow.collect { time -> + Log.d("TimerFragment", "Time updated: ${time.toDisplayString()}") + binding.time.text = time.toDisplayString() + } + } + } + with(binding) { - time.text = this@TimerFragment.time.toDisplayString() btnStart.setOnClickListener { started = true } @@ -70,16 +82,15 @@ class TimerFragment : Fragment() { override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putLong(TIME, time.inWholeMilliseconds) outState.putBoolean(STARTED, started) } private fun startTimer() { - // TODO: Start timer + timerUseCase.startTimer() } private fun stopTimer() { - // TODO: Stop timer + timerUseCase.stopTimer() } override fun onDestroyView() { @@ -88,15 +99,16 @@ class TimerFragment : Fragment() { } companion object { - private const val TIME = "time" private const val STARTED = "started" + 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 diff --git a/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerUsecase.kt b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerUsecase.kt new file mode 100644 index 0000000..e33353f --- /dev/null +++ b/app/src/main/kotlin/ru/otus/coroutineshomework/ui/timer/TimerUsecase.kt @@ -0,0 +1,66 @@ +package ru.otus.coroutineshomework.ui.timer + +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 + + +class TimerUseCase : CoroutineScope { + + override val coroutineContext: CoroutineContext = SupervisorJob() + + private val _timeFlow: MutableStateFlow = MutableStateFlow(Duration.ZERO) + val timeFlow = _timeFlow.asStateFlow() + + private var timerJob: Job? = null + + fun startTimer() { + 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) + } + } + } + + fun stopTimer() { + timerJob?.cancel() + timerJob = null + } + + companion object { + + private const val TIMER_DELAY_MS = 16L + + private val timerDelayMs = TIMER_DELAY_MS.milliseconds + + + } + +} + +@Module +@InstallIn(SingletonComponent::class) +class AppModule { + + @Provides + @Singleton + fun timerUseCase() = TimerUseCase() + +} \ 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