Revert "fix"

This reverts commit a04fb915ae.
This commit is contained in:
solovushka56
2025-12-12 12:34:33 +03:00
parent a04fb915ae
commit 2da3b58773
11 changed files with 294 additions and 443 deletions

View File

@@ -2,7 +2,6 @@ package ru.myitschool.work
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import ru.myitschool.work.data.DataStoreManager
import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.repo.AuthRepository
class App : Application() { class App : Application() {
@@ -17,13 +16,9 @@ class App : Application() {
} }
} }
lateinit var dataStoreManager: DataStoreManager
private set
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
dataStoreManager = DataStoreManager(applicationContext)
AuthRepository.getInstance(applicationContext) AuthRepository.getInstance(applicationContext)
} }

View File

@@ -1,36 +0,0 @@
package ru.myitschool.work.data
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "user_prefs")
class DataStoreManager(context: Context) {
private val dataStore = context.dataStore
private object Keys {
val USER_CODE = stringPreferencesKey("user_code")
}
suspend fun saveUserCode(code: String) {
dataStore.edit { prefs ->
prefs[Keys.USER_CODE] = code
}
}
suspend fun clearUserCode() {
dataStore.edit { prefs ->
prefs.remove(Keys.USER_CODE)
}
}
fun getUserCode(): Flow<String> = dataStore.data.map { prefs ->
prefs[Keys.USER_CODE] ?: ""
}
}

View File

@@ -2,13 +2,9 @@ package ru.myitschool.work.data.repo
import android.content.Context import android.content.Context
import android.content.Context.MODE_PRIVATE import android.content.Context.MODE_PRIVATE
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import ru.myitschool.work.App
import ru.myitschool.work.data.source.NetworkDataSource import ru.myitschool.work.data.source.NetworkDataSource
class AuthRepository private constructor(context: Context) { class AuthRepository private constructor(context: Context) {
@@ -50,9 +46,7 @@ class AuthRepository private constructor(context: Context) {
userCache = UserCache(name, photo) userCache = UserCache(name, photo)
_isAuthorized.value = true _isAuthorized.value = true
} else { } else {
CoroutineScope(Dispatchers.IO).launch { clear()
clear()
}
} }
} }
@@ -66,10 +60,9 @@ class AuthRepository private constructor(context: Context) {
getPrefs().edit() getPrefs().edit()
.putString(KEY_CODE, text) .putString(KEY_CODE, text)
.apply() .apply()
val app = context.applicationContext as App
app.dataStoreManager.saveUserCode(text)
} }
}.onFailure { exception ->
println("Auth error: ${exception.message}")
} }
} }
@@ -85,16 +78,13 @@ class AuthRepository private constructor(context: Context) {
fun getUserInfo(): UserCache? = userCache fun getUserInfo(): UserCache? = userCache
suspend fun clear() { fun clear() {
codeCache = null codeCache = null
userCache = null userCache = null
_isAuthorized.value = false _isAuthorized.value = false
getPrefs().edit() getPrefs().edit()
.clear() .clear()
.apply() .apply()
val app = context.applicationContext as App
app.dataStoreManager.clearUserCode()
} }
} }

View File

@@ -1,33 +0,0 @@
// domain/main/LoadDataUseCase.kt
package ru.myitschool.work.domain.main
import ru.myitschool.work.data.model.UserInfoResponse
import ru.myitschool.work.data.repo.MainRepository
import ru.myitschool.work.domain.main.entities.BookingInfo
import ru.myitschool.work.domain.main.entities.UserEntity
class LoadDataUseCase(
private val repository: ru.myitschool.work.data.repo.MainRepository
) {
suspend operator fun invoke(userCode: String): Result<UserEntity> {
return repository.getUserInfo().map { userInfoResponse ->
mapToUserEntity(userInfoResponse)
}
}
private fun mapToUserEntity(response: UserInfoResponse): UserEntity {
val bookings = response.bookings.map { bookingResponse ->
BookingInfo(
date = bookingResponse.date,
place = bookingResponse.place,
id = bookingResponse.bookingId
)
}
return UserEntity(
name = response.name,
photoUrl = response.photoUrl,
booking = bookings
)
}
}

View File

@@ -1,7 +0,0 @@
package ru.myitschool.work.domain.main.entities
data class BookingInfo(
val date: String,
val place: String,
val id: Int
)

View File

@@ -1,32 +0,0 @@
package ru.myitschool.work.domain.main.entities
import java.time.LocalDate
import java.time.format.DateTimeFormatter
data class UserEntity(
val name: String,
val photoUrl: String?,
val booking: List<BookingInfo>
) {
fun hasBookings(): Boolean = booking.isNotEmpty()
fun getSortedBookingsWithFormattedDate(): List<Triple<String, String, BookingInfo>>? {
if (booking.isEmpty()) return null
return booking.sortedBy { it.date }
.map { booking ->
val originalDate = booking.date
val formattedDate = formatDate(originalDate)
Triple(originalDate, formattedDate, booking)
}
}
private fun formatDate(dateStr: String): String {
return try {
val date = LocalDate.parse(dateStr, DateTimeFormatter.ISO_DATE)
date.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))
} catch (e: Exception) {
dateStr
}
}
}

View File

@@ -1,8 +1,8 @@
package ru.myitschool.work.ui.screen.main package ru.myitschool.work.ui.screen.main
sealed interface MainIntent { sealed interface MainIntent {
object LoadData : MainIntent
object Booking : MainIntent
object Logout : MainIntent object Logout : MainIntent
object Refresh : MainIntent
object AddBooking : MainIntent
data class ItemClick(val position: Int) : MainIntent
} }

View File

@@ -1,7 +1,6 @@
package ru.myitschool.work.ui.screen.main package ru.myitschool.work.ui.screen.main
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -12,30 +11,29 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
@@ -48,272 +46,235 @@ import ru.myitschool.work.ui.nav.BookScreenDestination
@Composable @Composable
fun MainScreen( fun MainScreen(
navController: NavController, viewModel: MainViewModel = viewModel(factory = MainViewModelFactory(LocalContext.current)),
viewModel: MainViewModel = viewModel() navController: NavController
) { ) {
val state by viewModel.uiState.collectAsState() val state by viewModel.uiState.collectAsState()
val shouldRefresh by navController.currentBackStackEntry
?.savedStateHandle
?.getStateFlow<Boolean>("shouldRefresh", false)
?.collectAsState() ?: remember { mutableStateOf(false) }
LaunchedEffect(shouldRefresh) {
if (shouldRefresh) {
viewModel.onIntent(MainIntent.Refresh)
navController.currentBackStackEntry?.savedStateHandle?.remove<Boolean>("shouldRefresh")
}
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.actionFlow.collect { action -> viewModel.actionFlow.collect { action ->
when(action) { when (action) {
is MainAction.Auth -> { is MainAction.NavigateToAuth -> {
navController.navigate(AuthScreenDestination) { navController.navigate(AuthScreenDestination) {
popUpTo(0) popUpTo(0)
} }
} }
is MainAction.Booking -> navController.navigate(BookScreenDestination) is MainAction.NavigateToBooking -> {
navController.navigate(BookScreenDestination)
}
} }
} }
} }
when(state) {
is MainState.Loading -> {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
CircularProgressIndicator(
modifier = Modifier.size(64.dp)
)
}
}
is MainState.Error -> {
ErrorContent(viewModel)
}
is MainState.Data -> {
DataContent(
viewModel,
userData = (state as MainState.Data).userData
)
}
}
}
@Composable
fun ErrorContent(viewModel: MainViewModel){
val configuration = LocalConfiguration.current
Box( Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
Column( when (val currentState = state) {
horizontalAlignment = Alignment.CenterHorizontally, is MainState.Loading -> {
modifier = Modifier CircularProgressIndicator(
.padding(15.dp) modifier = Modifier.align(Alignment.Center)
.fillMaxSize()
) {
Spacer(modifier = Modifier.height(80.dp))
Text(
text = stringResource(R.string.data_error_message),
modifier = Modifier.testTag(TestIds.Main.ERROR),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.height(20.dp))
Button(
onClick = { viewModel.onIntent(MainIntent.LoadData) },
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Main.REFRESH_BUTTON),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
) )
) {
Text(stringResource(R.string.main_refresh))
} }
} is MainState.Data -> {
} if (currentState.error != null) {
} Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = currentState.error,
color = Color.Red,
modifier = Modifier
.testTag(TestIds.Main.ERROR)
.padding(bottom = 16.dp)
)
Button(
onClick = { viewModel.onIntent(MainIntent.Refresh) },
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON)
) {
Text(stringResource(R.string.main_refresh))
}
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (!currentState.userPhotoUrl.isNullOrEmpty()) {
Image(
painter = rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current)
.data(currentState.userPhotoUrl)
.build()
),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.size(64.dp)
.testTag(TestIds.Main.PROFILE_IMAGE)
)
} else {
Icon(
painter = painterResource(id = R.drawable.github),
contentDescription = null,
modifier = Modifier
.size(64.dp)
.testTag(TestIds.Main.PROFILE_IMAGE)
)
}
@Composable Spacer(modifier = Modifier.size(16.dp))
fun DataContent(
viewModel: MainViewModel,
userData: ru.myitschool.work.domain.main.entities.UserEntity
) {
val configuration = LocalConfiguration.current
Column ( Column {
horizontalAlignment = Alignment.CenterHorizontally, Text(
modifier = Modifier text = currentState.userName,
.fillMaxSize() style = MaterialTheme.typography.titleLarge,
.background(MaterialTheme.colorScheme.surfaceVariant) modifier = Modifier.testTag(TestIds.Main.PROFILE_NAME)
) { )
Column( if (currentState.bookings.isNotEmpty()) {
horizontalAlignment = Alignment.CenterHorizontally, Spacer(modifier = Modifier.height(4.dp))
modifier = Modifier Text(
.clip(RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp)) text = "Забронировано мест: ${currentState.bookings.size}",
.background(MaterialTheme.colorScheme.primary) style = MaterialTheme.typography.bodyMedium,
.fillMaxWidth() color = MaterialTheme.colorScheme.onSurfaceVariant
.padding(16.dp) )
) { }
Row( }
horizontalArrangement = Arrangement.SpaceBetween, }
modifier = Modifier.fillMaxWidth() }
) {
Button(
onClick = { viewModel.onIntent(MainIntent.LoadData) },
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
)
) {
Text(stringResource(R.string.main_refresh))
}
Button( Row(
onClick = { viewModel.onIntent(MainIntent.Logout) }, modifier = Modifier
modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON), .fillMaxWidth()
colors = ButtonDefaults.buttonColors( .padding(bottom = 16.dp),
containerColor = MaterialTheme.colorScheme.primary, horizontalArrangement = Arrangement.spacedBy(8.dp)
contentColor = MaterialTheme.colorScheme.onPrimary ) {
) Button(
) { onClick = { viewModel.onIntent(MainIntent.Logout) },
Text(stringResource(R.string.main_logout)) modifier = Modifier
.weight(1f)
.testTag(TestIds.Main.LOGOUT_BUTTON)
) {
Text(stringResource(R.string.main_logout))
}
Button(
onClick = { viewModel.onIntent(MainIntent.Refresh) },
modifier = Modifier
.weight(1f)
.testTag(TestIds.Main.REFRESH_BUTTON)
) {
Text(stringResource(R.string.main_refresh))
}
Button(
onClick = { viewModel.onIntent(MainIntent.AddBooking) },
modifier = Modifier
.weight(1f)
.testTag(TestIds.Main.ADD_BUTTON)
) {
Text(stringResource(R.string.main_add_booking))
}
}
if (currentState.bookings.isNotEmpty()) {
Text(
text = "Мои бронирования:",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
itemsIndexed(currentState.bookings) { index, booking ->
BookingItem(
booking = booking,
position = index,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.testTag(TestIds.Main.getIdItemByPosition(index))
)
}
}
} else {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
contentAlignment = Alignment.Center
) {
Text(
text = "У вас нет активных бронирований",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
} }
} }
Image(
painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.data(userData.photoUrl)
.build()
),
contentDescription = stringResource(R.string.main_avatar_description),
modifier = Modifier
.clip(RoundedCornerShape(999.dp))
.testTag(TestIds.Main.PROFILE_IMAGE)
.size(150.dp)
.padding(20.dp),
contentScale = ContentScale.Crop
)
Text(
text = userData.name,
color = MaterialTheme.colorScheme.onPrimary,
textAlign = TextAlign.Center,
modifier = Modifier
.testTag(TestIds.Main.PROFILE_NAME)
.padding(horizontal = 20.dp),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(20.dp))
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.surface)
.padding(16.dp)
) {
Text(
text = "Мои бронирования:",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(bottom = 16.dp)
)
if (userData.hasBookings()) {
SortedBookingList(userData = userData)
} else {
EmptyBookings()
}
Spacer(modifier = Modifier.weight(1f))
Button(
onClick = { viewModel.onIntent(MainIntent.Booking) },
modifier = Modifier
.testTag(TestIds.Main.ADD_BUTTON)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
)
) {
Text(stringResource(R.string.main_add_booking))
}
} }
} }
} }
@Composable @Composable
fun SortedBookingList(userData: ru.myitschool.work.domain.main.entities.UserEntity) { private fun BookingItem(
val sortedBookings = remember(userData.booking) { booking: BookingItem,
userData.getSortedBookingsWithFormattedDate()?.sortedBy { (originalDate, _, _) -> position: Int,
originalDate modifier: Modifier = Modifier
}
}
LazyColumn(
modifier = Modifier
.fillMaxWidth()
) {
itemsIndexed(
items = sortedBookings ?: emptyList()
) { index, (originalDate, formattedDate, bookingInfo) ->
Box(
modifier = Modifier.testTag(TestIds.Main.getIdItemByPosition(index))
) {
BookingItem(
originalDate = originalDate,
formattedDate = formattedDate,
bookingInfo = bookingInfo,
index = index
)
}
Spacer(modifier = Modifier.height(8.dp))
}
}
}
@Composable
fun BookingItem(
originalDate: String,
formattedDate: String,
bookingInfo: ru.myitschool.work.domain.main.entities.BookingInfo,
index: Int
) { ) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = bookingInfo.place,
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE),
style = MaterialTheme.typography.bodyMedium
)
Text(
text = formattedDate,
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE),
style = MaterialTheme.typography.bodyMedium
)
}
}
@Composable
fun EmptyBookings() {
Box( Box(
modifier = Modifier modifier = modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) { ) {
Text( Card(
text = "У вас нет активных бронирований", modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium, elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
color = MaterialTheme.colorScheme.onSurfaceVariant ) {
) Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = booking.place,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE)
)
Text(
text = booking.getFormattedDate(),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE)
)
}
}
} }
} }

View File

@@ -5,8 +5,12 @@ import java.time.format.DateTimeFormatter
sealed interface MainState { sealed interface MainState {
object Loading : MainState object Loading : MainState
data class Data(val userData: ru.myitschool.work.domain.main.entities.UserEntity) : MainState data class Data(
object Error : MainState val userName: String = "",
val userPhotoUrl: String? = null,
val bookings: List<BookingItem> = emptyList(),
val error: String? = null
) : MainState
} }
data class BookingItem( data class BookingItem(

View File

@@ -1,97 +1,112 @@
package ru.myitschool.work.ui.screen.main package ru.myitschool.work.ui.screen.main
import android.app.Application import android.content.Context
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.myitschool.work.App import java.time.LocalDate
import java.time.format.DateTimeFormatter
import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.data.repo.MainRepository import ru.myitschool.work.data.repo.MainRepository
import ru.myitschool.work.domain.main.LoadDataUseCase
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val dataStoreManager by lazy {
(getApplication() as App).dataStoreManager
}
private val authRepository by lazy {
AuthRepository.getInstance(getApplication())
}
private val mainRepository by lazy {
MainRepository(authRepository)
}
private val loadDataUseCase by lazy {
LoadDataUseCase(mainRepository)
}
class MainViewModel(
private val authRepo: AuthRepository,
private val mainRepo: MainRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<MainState>(MainState.Loading) private val _uiState = MutableStateFlow<MainState>(MainState.Loading)
val uiState: StateFlow<MainState> = _uiState.asStateFlow() val uiState: StateFlow<MainState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<MainAction> = MutableSharedFlow() private val _actionFlow: MutableSharedFlow<MainAction> = MutableSharedFlow()
val actionFlow: SharedFlow<MainAction> = _actionFlow val actionFlow: SharedFlow<MainAction> = _actionFlow
private val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
init { init {
loadData() loadData()
} }
private fun loadData() { fun onIntent(intent: MainIntent) {
viewModelScope.launch(Dispatchers.IO) { when (intent) {
_uiState.update { MainState.Loading } MainIntent.Logout -> {
authRepo.clear()
try { viewModelScope.launch {
val userCode = dataStoreManager.getUserCode().first() _actionFlow.emit(MainAction.NavigateToAuth)
if (userCode.isEmpty()) {
_actionFlow.emit(MainAction.Auth)
return@launch
} }
}
loadDataUseCase.invoke(userCode).fold( MainIntent.Refresh -> {
onSuccess = { data -> loadData()
_uiState.update { MainState.Data(data) } }
}, MainIntent.AddBooking -> {
onFailure = { error -> viewModelScope.launch {
error.printStackTrace() _actionFlow.emit(MainAction.NavigateToBooking)
_uiState.update { MainState.Error } }
} }
) is MainIntent.ItemClick -> {
} catch (error: Exception) {
error.printStackTrace()
_uiState.update { MainState.Error }
} }
} }
} }
fun onIntent(intent: MainIntent) { private fun loadData() {
when(intent) { viewModelScope.launch {
is MainIntent.LoadData -> loadData() _uiState.update { MainState.Loading }
is MainIntent.Booking -> { mainRepo.getUserInfo().fold(
viewModelScope.launch(Dispatchers.Default) { onSuccess = { userInfo ->
_actionFlow.emit(MainAction.Booking) val bookings = userInfo.bookings.mapNotNull { bookingResponse ->
try {
BookingItem(
id = bookingResponse.bookingId.toString(),
date = LocalDate.parse(bookingResponse.date, dateFormatter),
place = bookingResponse.place
)
} catch (e: Exception) {
null
}
}.sortedBy { it.date }
authRepo.saveUserInfo(userInfo.name, userInfo.photoUrl)
_uiState.update {
MainState.Data(
userName = userInfo.name,
userPhotoUrl = userInfo.photoUrl,
bookings = bookings
)
}
},
onFailure = { error ->
_uiState.update {
MainState.Data(
userName = "",
userPhotoUrl = null,
bookings = emptyList(),
error = error.message ?: "Ошибка загрузки данных"
)
}
} }
} )
is MainIntent.Logout -> {
viewModelScope.launch(Dispatchers.IO) {
authRepository.clear()
_actionFlow.emit(MainAction.Auth)
}
}
} }
} }
} }
class MainViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
val authRepository = AuthRepository.getInstance(context)
val mainRepository = MainRepository(authRepository)
return MainViewModel(authRepository, mainRepository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
sealed interface MainAction { sealed interface MainAction {
object Auth : MainAction object NavigateToAuth : MainAction
object Booking : MainAction object NavigateToBooking : MainAction
} }

View File

@@ -13,10 +13,4 @@
<string name="book_book">Забронировать</string> <string name="book_book">Забронировать</string>
<string name="book_refresh">Повторить</string> <string name="book_refresh">Повторить</string>
<string name="book_empty">Всё забронировано</string> <string name="book_empty">Всё забронировано</string>
<string name="data_error_message">Ошибка загрузки данных</string>
<string name="main_update">Обновить</string>
<string name="main_booking_title">Мои бронирования</string>
<string name="main_empty_booking">У вас нет активных бронирований</string>
<string name="main_avatar_description">Аватар пользователя</string>
<string name="add_icon_description">Добавить</string>
</resources> </resources>