Full write logic of entries fragment

This commit is contained in:
Denis Oleynik 2025-02-20 14:28:45 +03:00
parent 438aca7b75
commit dbcb29ce79
17 changed files with 359 additions and 80 deletions

View File

@ -6,7 +6,9 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import ru.myitschool.work.data.repo.AccountRepositoryImpl
import ru.myitschool.work.data.repo.AuthorizationRepositoryImpl
import ru.myitschool.work.data.repo.EntryRepositoryImpl
import ru.myitschool.work.domain.auth.repo.AuthorizationRepository
import ru.myitschool.work.domain.entry.repo.EntryRepository
import ru.myitschool.work.domain.profile.repo.UserInfoRepository
@Module
@ -22,4 +24,9 @@ abstract class RepoModule {
abstract fun bindAccountRepo(
impl: AccountRepositoryImpl
): UserInfoRepository
@Binds
abstract fun bindEntryRepository(
impl: EntryRepositoryImpl
): EntryRepository
}

View File

@ -0,0 +1,17 @@
package ru.myitschool.work.data.dto
import com.google.gson.annotations.SerializedName
import java.time.LocalDateTime
class EntriesDto(
@SerializedName("id")
val id: Long,
@SerializedName("username")
val username: String,
@SerializedName("time")
val time: String,
@SerializedName("type")
val type: VisitType,
@SerializedName("readerId")
val readerId: String,
)

View File

@ -0,0 +1,5 @@
package ru.myitschool.work.data.dto
enum class VisitType {
SCANNER, NFC
}

View File

@ -0,0 +1,34 @@
package ru.myitschool.work.data.mapper
import android.os.Build
import androidx.annotation.RequiresApi
import ru.myitschool.work.data.dto.EntriesDto
import ru.myitschool.work.data.dto.UserInfoDto
import ru.myitschool.work.domain.profile.entities.UserInfoEntity
import ru.myitschool.work.ui.entrylist.adapter.EntryHistoryEntity
import java.text.SimpleDateFormat
import java.util.Locale
import javax.inject.Inject
class EntryInfoMapper @Inject constructor() {
@RequiresApi(Build.VERSION_CODES.O)
operator fun invoke(model: List<EntriesDto>): Result<List<EntryHistoryEntity>> {
return kotlin.runCatching {
model.map {
EntryHistoryEntity(
type = it.type.name,
identificator = it.readerId,
time = it.time.toString(),
)
}.toList()
}
}
private companion object {
private val simpleDateFormat = SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss",
Locale.US
)
}
}

View File

@ -0,0 +1,31 @@
package ru.myitschool.work.data.repo
import dagger.Lazy
import dagger.Reusable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ru.myitschool.work.data.mapper.EntryInfoMapper
import ru.myitschool.work.data.mapper.UserInfoMapper
import ru.myitschool.work.data.source.AccountNetworkDataSource
import ru.myitschool.work.data.source.EntryNetworkDataSource
import ru.myitschool.work.domain.entry.entities.EntryEntity
import ru.myitschool.work.domain.entry.repo.EntryRepository
import ru.myitschool.work.domain.profile.entities.UserInfoEntity
import ru.myitschool.work.domain.profile.repo.UserInfoRepository
import ru.myitschool.work.ui.entrylist.adapter.EntryHistoryEntity
import javax.inject.Inject
@Reusable
class EntryRepositoryImpl @Inject constructor(
private val entryNetworkDataSource: EntryNetworkDataSource,
private val entryInfoMapper: Lazy<EntryInfoMapper>,
): EntryRepository {
override suspend fun getEntries(basicAuth: String): Result<List<EntryHistoryEntity>> {
return withContext(Dispatchers.IO) {
entryNetworkDataSource.getEntries(basicAuth).fold(
onSuccess = { value -> entryInfoMapper.get().invoke(value) },
onFailure = { error -> Result.failure(error) }
)
}
}
}

View File

@ -0,0 +1,12 @@
package ru.myitschool.work.data.source
import retrofit2.http.GET
import retrofit2.http.Header
import ru.myitschool.work.data.dto.EntriesDto
interface EntryApi {
@GET("api/employee/visits")
suspend fun getEntries(
@Header("Authorization") basicAuth: String
) : List<EntriesDto>
}

View File

@ -0,0 +1,21 @@
package ru.myitschool.work.data.source
import dagger.Reusable
import retrofit2.Retrofit
import ru.myitschool.work.data.dto.EntriesDto
import ru.myitschool.work.data.dto.OpenQrDto
import ru.myitschool.work.data.dto.UserInfoDto
import javax.inject.Inject
@Reusable
class EntryNetworkDataSource @Inject constructor(
private val retrofit: Retrofit
) {
private val api by lazy {
retrofit.create(EntryApi::class.java)
}
suspend fun getEntries(basicAuth: String): Result<List<EntriesDto>> {
return kotlin.runCatching { api.getEntries(basicAuth = basicAuth) }
}
}

View File

@ -7,9 +7,6 @@ import javax.inject.Inject
class LoginUseCase @Inject constructor(
private val repo: AuthorizationRepository,
) {
// suspend operator fun invoke(login: String): Result<Unit> {
// return repo.login(username = login)
// }
suspend operator fun invoke(data: LoginDto): Result<Unit>{
return repo.login(data=data)
}

View File

@ -0,0 +1,18 @@
package ru.myitschool.work.domain.entry
import ru.myitschool.work.domain.auth.GetLoginUseCase
import ru.myitschool.work.domain.entry.repo.EntryRepository
import ru.myitschool.work.ui.entrylist.adapter.EntryHistoryEntity
import javax.inject.Inject
class EntryListUseCase @Inject constructor(
private val repo: EntryRepository,
private val getLoginUseCase: GetLoginUseCase,
) {
suspend operator fun invoke(): Result<List<EntryHistoryEntity>> {
return getLoginUseCase().fold(
onSuccess = { basicAuth -> repo.getEntries(basicAuth) },
onFailure = { error -> Result.failure(error) }
)
}
}

View File

@ -0,0 +1,10 @@
package ru.myitschool.work.domain.entry.entities
import java.time.LocalDateTime
class EntryEntity(
val username: String,
val time: LocalDateTime,
val type: EntryType,
val readerId: String,
)

View File

@ -0,0 +1,5 @@
package ru.myitschool.work.domain.entry.entities
enum class EntryType {
SCANNER, NFC
}

View File

@ -0,0 +1,7 @@
package ru.myitschool.work.domain.entry.repo
import ru.myitschool.work.ui.entrylist.adapter.EntryHistoryEntity
interface EntryRepository {
suspend fun getEntries(basicAuth: String) : Result<List<EntryHistoryEntity>>
}

View File

@ -1,5 +1,6 @@
package ru.myitschool.work.ui.entrylist
import android.content.res.ColorStateList
import androidx.fragment.app.viewModels
import android.os.Bundle
import android.util.Log
@ -8,8 +9,11 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.core.content.ContextCompat
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.squareup.picasso.Picasso
import dagger.hilt.android.AndroidEntryPoint
import ru.myitschool.work.R
import ru.myitschool.work.databinding.FragmentEntryListBinding
@ -21,6 +25,8 @@ import ru.myitschool.work.ui.login.LoginViewModel
import ru.myitschool.work.ui.profile.ProfileDestination
import ru.myitschool.work.ui.profile.ProfileViewModel
import ru.myitschool.work.utils.collectWhenStarted
import ru.myitschool.work.utils.visibleOrGone
@AndroidEntryPoint
class EntryListFragment : Fragment(R.layout.fragment_entry_list) {
@ -29,22 +35,49 @@ class EntryListFragment : Fragment(R.layout.fragment_entry_list) {
private val viewModel: EntryListViewModel by viewModels()
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentEntryListBinding.bind(view)
initCallback()
subscribe()
swipeRefreshLayout = binding.swipeRefreshLayout2
swipeRefreshLayout.setOnRefreshListener {
viewModel.updateEntryList()
}
}
private fun initCallback() {
binding.floatingActionButton2.setOnClickListener { viewModel.closeEntryList() }
binding.recyclerView.adapter = AdapterEntryHistory(viewModel.getEntryList())
// binding.recyclerView.adapter = AdapterEntryHistory(viewModel.getEntryList())
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
}
private fun subscribe() {
viewModel.state.collectWhenStarted(this) { state ->
binding.recyclerView.visibleOrGone(state is EntryListViewModel.State.Show)
binding.error2.visibleOrGone(state is EntryListViewModel.State.Error)
when(state) {
is EntryListViewModel.State.Loading -> {
swipeRefreshLayout.isRefreshing = true
}
is EntryListViewModel.State.Error -> {
swipeRefreshLayout.isRefreshing = false
binding.error2.text = state.errorText
}
is EntryListViewModel.State.Show -> {
swipeRefreshLayout.isRefreshing = false
binding.recyclerView.adapter = AdapterEntryHistory(state.entryList)
}
}
}
viewModel.action.collectWhenStarted(this) { action ->
when (action) {
is EntryListViewModel.Action.OpenProfile -> {

View File

@ -3,31 +3,38 @@ package ru.myitschool.work.ui.entrylist
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.Lazy
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.myitschool.work.R
import ru.myitschool.work.domain.entry.EntryListUseCase
import ru.myitschool.work.ui.entrylist.adapter.EntryHistoryEntity
import ru.myitschool.work.ui.profile.ProfileViewModel
import ru.myitschool.work.ui.profile.ProfileViewModel.Action
import ru.myitschool.work.ui.profile.ProfileViewModel.State
import ru.myitschool.work.utils.MutablePublishFlow
import java.text.SimpleDateFormat
import java.util.Locale
import javax.inject.Inject
@HiltViewModel
class EntryListViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val entryUseCase: Lazy<EntryListUseCase>,
) : ViewModel() {
private val _action = MutablePublishFlow<Action>()
val action = _action.asSharedFlow()
private val _state = MutableStateFlow<EntryListViewModel.State>(initialState)
val state = _state.asStateFlow()
init {
updateEntryList()
}
// Новый список данных
private val entryList = listOf(
private var entryList = listOf(
EntryHistoryEntity("type1", "2023-10-01T12:00:00", "id001"),
EntryHistoryEntity("type2", "2023-10-02T13:00:00", "id002"),
EntryHistoryEntity("type3", "2023-10-03T14:00:00", "id003"),
@ -36,8 +43,28 @@ class EntryListViewModel @Inject constructor(
)
// Функция для получения данных
fun getEntryList(): List<EntryHistoryEntity> {
return entryList
fun updateEntryList() {
viewModelScope.launch {
_state.update { State.Loading }
entryUseCase.get().invoke().fold(
onSuccess = { entryListEntity ->
entryList = entryListEntity
_state.update {
State.Show(
entryList = entryListEntity
)
}
},
onFailure = { error ->
_state.update {
State.Error(
errorText = error.localizedMessage
?: context.resources.getString(R.string.login_error)
)
}
}
)
}
}
fun closeEntryList(){
@ -49,4 +76,20 @@ class EntryListViewModel @Inject constructor(
sealed interface Action {
data object OpenProfile: Action
}
sealed interface State {
data object Loading : State
data class Error(
val errorText: String,
) : State
data class Show(
val entryList: List<EntryHistoryEntity>
) : State
}
private companion object {
val initialState = State.Loading
}
}

View File

@ -36,8 +36,8 @@ class AdapterEntryHistory(private val datas: List<EntryHistoryEntity>) : Recycle
holder.dateText.text = LocalDateTime.parse(datas[position].time).format(DateTimeFormatter.ofPattern("d MMMM yyyy, HH:mm:ss", Locale.getDefault()))
holder.identificatorText.text = datas[position].identificator.toString()
holder.typeText.text = datas[position].type.toString()
holder.identificatorText.text = datas[position].identificator
holder.typeText.text = if (datas[position].type == "SCANNER") "Сканер (Телефон)" else "NFC-карта"
}

View File

@ -1,40 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/frameLayout2"
android:id="@+id/swipeRefreshLayout2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".ui.entrylist.EntryListFragment">
android:layout_height="match_parent">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatingActionButton2"
style="@style/Theme.UiTemplate.FAB.Gray"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:src="@drawable/ic_back"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".ui.entrylist.EntryListFragment">
<TextView
android:id="@+id/textView5"
style="@style/Theme.UiTemplate.TextH2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/entry_history"
app:layout_constraintBottom_toBottomOf="@+id/floatingActionButton2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/floatingActionButton2"
app:layout_constraintTop_toTopOf="@+id/floatingActionButton2" />
<TextView
android:id="@+id/error2"
style="@style/Theme.UiTemplate.TextH5"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:textAlignment="center"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Something wrong. Try later" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/floatingActionButton2" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatingActionButton2"
style="@style/Theme.UiTemplate.FAB.Gray"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:src="@drawable/ic_back"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView5"
style="@style/Theme.UiTemplate.TextH2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/entry_history"
app:layout_constraintBottom_toBottomOf="@+id/floatingActionButton2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/floatingActionButton2"
app:layout_constraintTop_toTopOf="@+id/floatingActionButton2" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/floatingActionButton2" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -1,40 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/frameLayout2"
android:id="@+id/swipeRefreshLayout2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".ui.entrylist.EntryListFragment">
android:layout_height="match_parent">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatingActionButton2"
style="@style/Theme.UiTemplate.FAB.Gray"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:src="@drawable/ic_back"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".ui.entrylist.EntryListFragment">
<TextView
android:id="@+id/textView5"
style="@style/Theme.UiTemplate.TextH2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/entry_history"
app:layout_constraintBottom_toBottomOf="@+id/floatingActionButton2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/floatingActionButton2"
app:layout_constraintTop_toTopOf="@+id/floatingActionButton2" />
<TextView
android:id="@+id/error2"
style="@style/Theme.UiTemplate.TextH5"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:textAlignment="center"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Something wrong. Try later" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/floatingActionButton2" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatingActionButton2"
style="@style/Theme.UiTemplate.FAB.Gray"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:src="@drawable/ic_back"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView5"
style="@style/Theme.UiTemplate.TextH2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/entry_history"
app:layout_constraintBottom_toBottomOf="@+id/floatingActionButton2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/floatingActionButton2"
app:layout_constraintTop_toTopOf="@+id/floatingActionButton2" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/floatingActionButton2" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>