Impl base schedule navigation; add internal logger

This commit is contained in:
Shcherbatykh Oleg
2026-02-19 13:40:00 +03:00
parent 592d948cd0
commit 6aa4b6d39d
22 changed files with 180 additions and 72 deletions

View File

@@ -3,8 +3,6 @@ package ru.fincode.tsudesk
import android.app.Application import android.app.Application
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
const val LOG_DEBUG_TAG = "NETWORK_DEBUG"
@HiltAndroidApp @HiltAndroidApp
class App : Application() { class App : Application() {
override fun onCreate() { override fun onCreate() {

View File

@@ -6,8 +6,8 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.navigation import androidx.navigation.compose.navigation
import ru.fincode.tsudesk.core.navigation.AppRoute import ru.fincode.tsudesk.core.navigation.AppRoute
import ru.fincode.tsudesk.feature.schedule.ui.navigation.scheduleGraph import ru.fincode.tsudesk.feature.schedule.presentation.navigation.scheduleGraph
import ru.fincode.tsudesk.feature.splash.ui.navigation.splashGraph import ru.fincode.tsudesk.feature.splash.presentation.navigation.splashGraph
@Composable @Composable
fun AppNavHost( fun AppNavHost(

View File

@@ -0,0 +1,5 @@
package ru.fincode.tsudesk.core.common.log
object Constants {
const val LOG_DEBUG_TAG = "LOG_DEBUG_TAG"
}

View File

@@ -0,0 +1,25 @@
package ru.fincode.tsudesk.core.common.log
import android.util.Log
private fun Any.moduleTag(): String {
val module = this::class.java.`package`?.name?.substringAfterLast(".")?.uppercase() ?: "UNKNOWN"
return "${Constants.LOG_DEBUG_TAG}:$module"
}
fun Any.logD(message: String) {
Log.d(moduleTag(), message)
}
fun Any.logD(message: String, throwable: Throwable) {
Log.d(moduleTag(), message, throwable)
}
fun Any.logE(message: String) {
Log.e(moduleTag(), message)
}
fun Any.logE(message: String, throwable: Throwable) {
Log.e(moduleTag(), message, throwable)
}

View File

@@ -1,5 +0,0 @@
package ru.fincode.tsudesk.core.network
object Constants {
}

View File

@@ -3,20 +3,18 @@ package ru.fincode.tsudesk.core.network.interceptor
import android.util.Log import android.util.Log
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import ru.fincode.tsudesk.core.common.log.logD
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
private const val LOG_DEBUG_TAG = "LOG_DEBUG_TAG"
@Singleton @Singleton
class DebugInterceptor @Inject constructor() : Interceptor { class DebugInterceptor @Inject constructor() : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request() val request = chain.request()
logD("URL: ${request.url}")
Log.d(LOG_DEBUG_TAG, "URL: ${request.url}") logD("Method: ${request.method}")
Log.d(LOG_DEBUG_TAG, "Method: ${request.method}")
return chain.proceed(request) return chain.proceed(request)
} }
} }

View File

@@ -3,18 +3,18 @@ package ru.fincode.tsudesk.core.network.interceptor
import android.util.Log import android.util.Log
import okhttp3.Call import okhttp3.Call
import okhttp3.EventListener import okhttp3.EventListener
import ru.fincode.tsudesk.core.common.log.logD
import java.io.IOException import java.io.IOException
import java.net.InetAddress import java.net.InetAddress
private const val LOG_DEBUG_TAG = "LOG_DEBUG_TAG"
class NetEventLogger : EventListener() { class NetEventLogger : EventListener() {
override fun dnsStart(call: Call, domainName: String) { override fun dnsStart(call: Call, domainName: String) {
Log.d(LOG_DEBUG_TAG, "dnsStart: $domainName") logD("dnsStart: $domainName")
} }
override fun dnsEnd(call: Call, domainName: String, inetAddressList: List<InetAddress>) { override fun dnsEnd(call: Call, domainName: String, inetAddressList: List<InetAddress>) {
Log.d(LOG_DEBUG_TAG, "dnsEnd: $domainName -> $inetAddressList") logD("dnsEnd: $domainName -> $inetAddressList")
} }
override fun connectStart( override fun connectStart(
@@ -22,30 +22,30 @@ class NetEventLogger : EventListener() {
inetSocketAddress: java.net.InetSocketAddress, inetSocketAddress: java.net.InetSocketAddress,
proxy: java.net.Proxy proxy: java.net.Proxy
) { ) {
Log.d(LOG_DEBUG_TAG, "connectStart: $inetSocketAddress proxy=$proxy") logD("connectStart: $inetSocketAddress proxy=$proxy")
} }
override fun secureConnectStart(call: Call) { override fun secureConnectStart(call: Call) {
Log.d(LOG_DEBUG_TAG, "tlsStart") logD("tlsStart")
} }
override fun secureConnectEnd(call: Call, handshake: okhttp3.Handshake?) { override fun secureConnectEnd(call: Call, handshake: okhttp3.Handshake?) {
Log.d(LOG_DEBUG_TAG, "tlsEnd: $handshake") logD("tlsEnd: $handshake")
} }
override fun requestHeadersStart(call: Call) { override fun requestHeadersStart(call: Call) {
Log.d(LOG_DEBUG_TAG, "reqHeadersStart") logD("reqHeadersStart")
} }
override fun responseHeadersStart(call: Call) { override fun responseHeadersStart(call: Call) {
Log.d(LOG_DEBUG_TAG, "respHeadersStart") logD("respHeadersStart")
} }
override fun callFailed(call: Call, ioe: IOException) { override fun callFailed(call: Call, ioe: IOException) {
Log.e(LOG_DEBUG_TAG, "callFailed", ioe) logD("callFailed", ioe)
} }
override fun callEnd(call: Call) { override fun callEnd(call: Call) {
Log.d(LOG_DEBUG_TAG, "callEnd") logD("callEnd")
} }
} }

View File

@@ -8,7 +8,6 @@ import ru.fincode.tsudesk.core.network.model.NetworkResult
import ru.fincode.tsudesk.core.network.model.map import ru.fincode.tsudesk.core.network.model.map
import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleLocalDataSource import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleLocalDataSource
import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleRemoteDataSource import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleRemoteDataSource
import ru.fincode.tsudesk.feature.schedule.data.local.ScheduleCacheKey
import ru.fincode.tsudesk.feature.schedule.data.remote.ScheduleNetworkMapper import ru.fincode.tsudesk.feature.schedule.data.remote.ScheduleNetworkMapper
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleType import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleType
@@ -23,9 +22,11 @@ class ScheduleRepositoryImpl @Inject constructor(
override fun observeSchedule(type: ScheduleType): Flow<DataResult<ScheduleEntity>> = flow { override fun observeSchedule(type: ScheduleType): Flow<DataResult<ScheduleEntity>> = flow {
val scheduleKey = type.toCacheKey() val scheduleKey = type.toCacheKey()
val cached = local.observeSchedule(scheduleKey).first() val cached = local.observeSchedule(scheduleKey).first()
cached?.let { if (cached != null) {
emit(DataResult.Data(it, refreshedFromNetwork = false)) emit(DataResult.Data(cached, refreshedFromNetwork = false))
if ((System.currentTimeMillis() - cached.timestamp) < CACHE_TTL_MS) return@flow
} }
val networkResult = loadSchedule(type) val networkResult = loadSchedule(type)
@@ -45,11 +46,12 @@ class ScheduleRepositoryImpl @Inject constructor(
private suspend fun loadSchedule(type: ScheduleType): NetworkResult<ScheduleEntity> = private suspend fun loadSchedule(type: ScheduleType): NetworkResult<ScheduleEntity> =
when (type) { when (type) {
is ScheduleType.Group -> is ScheduleType.Group -> remote.loadScheduleByGroup(type.number).map(mapper::invoke)
remote.loadScheduleByGroup(type.number).map(mapper::invoke)
is ScheduleType.Teacher -> is ScheduleType.Teacher -> remote.loadScheduleByTeacher(type.name).map(mapper::invoke)
remote.loadScheduleByTeacher(type.name).map(mapper::invoke)
} }
private companion object {
private const val CACHE_TTL_MS: Long = 1;//24L * 60L * 60L * 1000L // 1 day
}
} }

View File

@@ -7,7 +7,7 @@ import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleType
import ru.fincode.tsudesk.feature.schedule.domain.repository.ScheduleRepository import ru.fincode.tsudesk.feature.schedule.domain.repository.ScheduleRepository
import javax.inject.Inject import javax.inject.Inject
class GetScheduleUseCase @Inject constructor( class ObserveScheduleUseCase @Inject constructor(
private val repository: ScheduleRepository private val repository: ScheduleRepository
) { ) {
operator fun invoke(type: ScheduleType): Flow<DataResult<ScheduleEntity>> = operator fun invoke(type: ScheduleType): Flow<DataResult<ScheduleEntity>> =

View File

@@ -0,0 +1,11 @@
package ru.fincode.tsudesk.feature.schedule.presentation.model
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity
data class ScheduleUiState(
val isLoading: Boolean = false,
val data: ScheduleEntity? = null,
val errorMessage: String? = null,
val title: String = "Schedule",
val refreshedFromNetwork: Boolean = false
)

View File

@@ -0,0 +1,20 @@
package ru.fincode.tsudesk.feature.schedule.presentation.navigation
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import ru.fincode.tsudesk.core.navigation.AppRoute
import ru.fincode.tsudesk.feature.schedule.presentation.screen.ScheduleRoute
fun NavGraphBuilder.scheduleGraph(
navController: NavHostController
) {
composable<AppRoute.Schedule> {
ScheduleRoute()
}
// composable<AppRoute.ScheduleDetails> { backStackEntry ->
// val args = backStackEntry.toRoute<AppRoute.ScheduleDetails>()
// ScheduleDetailsRoute(lessonId = args.lessonId)
// }
}

View File

@@ -0,0 +1,16 @@
package ru.fincode.tsudesk.feature.schedule.presentation.screen
import androidx.compose.runtime.Composable
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ru.fincode.tsudesk.feature.schedule.presentation.screen.ScheduleViewModel
import ru.fincode.tsudesk.feature.schedule.presentation.ui.ScheduleScreen
@Composable
fun ScheduleRoute(
viewModel: ScheduleViewModel = hiltViewModel()
) {
val state = viewModel.state.collectAsStateWithLifecycle().value
ScheduleScreen(state = state)
}

View File

@@ -2,21 +2,26 @@ package ru.fincode.tsudesk.feature.schedule.presentation.ui
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import ru.fincode.tsudesk.feature.schedule.presentation.model.ScheduleUiState
@Composable @Composable
fun ScheduleScreen() { fun ScheduleScreen(
state: ScheduleUiState
) {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( if (state.isLoading) {
text = "Schedule", CircularProgressIndicator()
color = Color.Black } else {
) Text(text = state.title, color = Color.Black)
}
} }
} }

View File

@@ -0,0 +1,50 @@
package ru.fincode.tsudesk.feature.schedule.presentation.screen
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import ru.fincode.tsudesk.core.common.log.logD
import ru.fincode.tsudesk.core.common.model.DataResult
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleType
import ru.fincode.tsudesk.feature.schedule.domain.usecase.ObserveScheduleUseCase
import ru.fincode.tsudesk.feature.schedule.presentation.model.ScheduleUiState
import javax.inject.Inject
@HiltViewModel
class ScheduleViewModel @Inject constructor(
private val observeScheduleUseCase: ObserveScheduleUseCase
) : ViewModel() {
private val scheduleType = ScheduleType.Group(number = "220631")
val state: StateFlow<ScheduleUiState> = observeScheduleUseCase(scheduleType).map { result ->
when (result) {
is DataResult.Data -> {
logD("Data loaded from ${if (result.refreshedFromNetwork) "NETWORK" else "CACHE"}")
ScheduleUiState(
isLoading = false,
data = result.data,
refreshedFromNetwork = result.refreshedFromNetwork
)
}
is DataResult.Error -> {
logD("Error loading schedule: ${result.error}")
ScheduleUiState(isLoading = false, errorMessage = result.error.toString())
}
}
}.onStart {
logD("Start collecting schedule flow")
emit(ScheduleUiState(isLoading = true))
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = ScheduleUiState(isLoading = true)
)
}

View File

@@ -1,21 +0,0 @@
package ru.fincode.tsudesk.feature.schedule.ui.navigation
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import ru.fincode.tsudesk.core.navigation.AppRoute
import ru.fincode.tsudesk.feature.schedule.presentation.ui.ScheduleScreen
fun NavGraphBuilder.scheduleGraph(
navController: NavHostController
) {
composable<AppRoute.Schedule> {
ScheduleScreen()
}
// регистрация экрана детальной информации о занятии
// composable<AppRoute.ScheduleDetails> { backStackEntry ->
// // val args = backStackEntry.toRoute<AppRoute.ScheduleDetails>()
// // ScheduleDetailsRoute(lessonId = args.lessonId)
// }
}

View File

@@ -1,5 +1,5 @@
package ru.fincode.tsudesk.feature.splash.ui package ru.fincode.tsudesk.feature.splash.presentation.model
sealed interface SplashEffect { sealed interface SplashEffect {
data object OpenMain : SplashEffect data object OpenMain : SplashEffect
} }

View File

@@ -0,0 +1,7 @@
package ru.fincode.tsudesk.feature.splash.presentation.model
data class SplashUiState(
val title: String = "SPLASH SCREEN",
val isLoading: Boolean = true,
val errorMessage: String? = null
)

View File

@@ -1,10 +1,10 @@
package ru.fincode.tsudesk.feature.splash.ui.navigation package ru.fincode.tsudesk.feature.splash.presentation.navigation
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import ru.fincode.tsudesk.core.navigation.AppRoute import ru.fincode.tsudesk.core.navigation.AppRoute
import ru.fincode.tsudesk.feature.splash.ui.SplashRoute import ru.fincode.tsudesk.feature.splash.presentation.screen.SplashRoute
fun NavGraphBuilder.splashGraph( fun NavGraphBuilder.splashGraph(
navController: NavHostController navController: NavHostController

View File

@@ -1,9 +1,10 @@
package ru.fincode.tsudesk.feature.splash.ui package ru.fincode.tsudesk.feature.splash.presentation.screen
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ru.fincode.tsudesk.feature.splash.presentation.model.SplashEffect
@Composable @Composable
fun SplashRoute( fun SplashRoute(

View File

@@ -1,4 +1,4 @@
package ru.fincode.tsudesk.feature.splash.ui package ru.fincode.tsudesk.feature.splash.presentation.screen
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import ru.fincode.tsudesk.feature.splash.presentation.model.SplashUiState
@Composable @Composable
fun SplashScreen( fun SplashScreen(

View File

@@ -1,4 +1,4 @@
package ru.fincode.tsudesk.feature.splash.ui package ru.fincode.tsudesk.feature.splash.presentation.screen
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@@ -10,6 +10,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.fincode.tsudesk.core.common.model.DataResult import ru.fincode.tsudesk.core.common.model.DataResult
import ru.fincode.tsudesk.core.config.domain.usecase.GetConfigUseCase import ru.fincode.tsudesk.core.config.domain.usecase.GetConfigUseCase
import ru.fincode.tsudesk.feature.splash.presentation.model.SplashUiState
import ru.fincode.tsudesk.feature.splash.presentation.model.SplashEffect
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel

View File

@@ -1,7 +0,0 @@
package ru.fincode.tsudesk.feature.splash.ui
data class SplashUiState(
val title: String = "TSUDesk",
val isLoading: Boolean = true,
val errorMessage: String? = null
)