From 56949ef282209c14956f8f6670766108da92eb75 Mon Sep 17 00:00:00 2001 From: magicsk Date: Tue, 3 Oct 2023 00:58:37 +0200 Subject: [PATCH] feat: Timetables loading and error handling --- .../eu/magicsk/transi/TimetableFragment.kt | 266 ++++++++++-------- .../eu/magicsk/transi/TimetablesFragment.kt | 12 +- .../transi/view_models/TimetablesViewModel.kt | 39 ++- app/src/main/res/drawable/ic_departure.xml | 5 + .../main/res/layout/fragment_timetable.xml | 93 +++++- app/src/main/res/values/strings.xml | 2 + 6 files changed, 277 insertions(+), 140 deletions(-) create mode 100644 app/src/main/res/drawable/ic_departure.xml diff --git a/app/src/main/java/eu/magicsk/transi/TimetableFragment.kt b/app/src/main/java/eu/magicsk/transi/TimetableFragment.kt index dff7d41..f6468e9 100644 --- a/app/src/main/java/eu/magicsk/transi/TimetableFragment.kt +++ b/app/src/main/java/eu/magicsk/transi/TimetableFragment.kt @@ -4,15 +4,20 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import eu.magicsk.transi.adapters.TimetableAdapter +import eu.magicsk.transi.data.remote.responses.idsbk.Session import eu.magicsk.transi.databinding.FragmentTimetableBinding import eu.magicsk.transi.util.* import eu.magicsk.transi.view_models.MainViewModel import eu.magicsk.transi.view_models.TimetablesViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class TimetableFragment : Fragment() { @@ -21,9 +26,7 @@ class TimetableFragment : Fragment() { override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentTimetableBinding.inflate(inflater, container, false) return binding.root @@ -40,139 +43,170 @@ class TimetableFragment : Fragment() { val lineNum = requireArguments().getString("short_name") ?: "Error" val lineDirections = requireArguments().getString("long_name") ?: "Error" binding.apply { + TimetableError.isVisible = false + TimetableNoDeparturesInfo.isVisible = false + val timetablesViewModel = + ViewModelProvider(requireActivity())[TimetablesViewModel::class.java] val context = root.context val resources = root.resources customizeLineText(TimetableTitleLine, lineNum, context, resources) TimetableTitleDirections.text = lineDirections TimetableTitleDirections.isSelected = true - val timetablesViewModel = - ViewModelProvider(requireActivity())[TimetablesViewModel::class.java] - val mainViewModel = - ViewModelProvider(requireActivity())[MainViewModel::class.java] + val mainViewModel = ViewModelProvider(requireActivity())[MainViewModel::class.java] mainViewModel.idsbkSession.observe(viewLifecycleOwner) { idsbkSession -> idsbkSession?.let { - timetablesViewModel.getTimetableDirections(routeId, idsbkSession) - timetablesViewModel.timetablesDirections.observe(viewLifecycleOwner) { data -> - val directions = data.directions - if (directions.isNotEmpty()) { - TimetableDirection1.text = directions[0].direction - if (directions.size > 1) TimetableDirection2.text = - directions[1].direction - TimetableDirectionToggle.addOnButtonCheckedListener { _, checkedId, isChecked -> - if (isChecked) { - when (checkedId) { - R.id.TimetableDirection1 -> { - animatedAlphaChange( - 0F, - 1F, - 0, - TimetableLoadingIndicator - ) - timetablesViewModel.getTimetable( - routeId, - "departures", - directions[0].direction_id, - getDate(), - idsbkSession - ) - } + TimetableErrorBtn.setOnClickListener { + TimetableError.isVisible = false + animatedAlphaChange(0F, 1F, 0, TimetableLoadingIndicator) + fetchTimetableDirection( + timetablesViewModel, routeId, idsbkSession, lineNum + ) + } + fetchTimetableDirection(timetablesViewModel, routeId, idsbkSession, lineNum) + } + } + } + } - R.id.TimetableDirection2 -> { - animatedAlphaChange( - 0F, - 1F, - 0, - TimetableLoadingIndicator - ) - timetablesViewModel.getTimetable( - routeId, - "departures", - directions[1].direction_id, - getDate(), - idsbkSession - ) - } + private fun fetchTimetableDirection( + timetablesViewModel: TimetablesViewModel, + routeId: Int, + idsbkSession: Session, + lineNum: String + ) { + CoroutineScope(Dispatchers.IO).launch { + val directions = + timetablesViewModel.getTimetableDirections(routeId, idsbkSession)?.directions + binding.apply { + CoroutineScope(Dispatchers.Main).launch { + if (!directions.isNullOrEmpty()) { + TimetableDirection1.text = directions[0].direction + if (directions.size > 1) { + TimetableDirection2.isVisible = true + TimetableDirection2.text = directions[1].direction + } else { + TimetableDirection2.isVisible = false + } - } - } - } - TimetableDirectionToggle.clearChecked() - TimetableDirectionToggle.check(R.id.TimetableDirection1) - timetablesViewModel.timetable.observe(viewLifecycleOwner) { timetableData -> - if (timetableData.departures.isNotEmpty()) { - val departures = timetableData.departures - TimetableTimeSlider.valueFrom = 0F - TimetableTimeSlider.valueTo = (departures.size - 1).toFloat() - TimetableTimeSlider.value = 0F - TimetableTimeSlider.stepSize = 1F - val minutes = getMinutes() - var tooLate = true - departures.forEachIndexed { i, d -> - if (d.departure - minutes > 1 && tooLate) { - tooLate = false - TimetableTimeSlider.value = i.toFloat() - } - } - if (tooLate) TimetableTimeSlider.value = - (departures.size - 1).toFloat() - TimetableTimeSlider.setLabelFormatter { value -> - val departureTime = departures[value.toInt()].departure - return@setLabelFormatter "${departureTime / 60}:${ - (departureTime % 60).toString().padStart(2, Char(48)) - }" - } - fun onTimetableStopClick(stationId: Int, stationName: String) { - val timetableDetailBundle = Bundle() - timetableDetailBundle.putInt("route_id", routeId) - timetableDetailBundle.putString("short_name", lineNum) - val direction = - if (TimetableDirectionToggle.checkedButtonId == R.id.TimetableDirection1) directions[0] else directions[1] - timetableDetailBundle.putString( - "long_name", - direction.direction + val timetableDetailBundle = Bundle() + timetableDetailBundle.putInt("route_id", routeId) + timetableDetailBundle.putString("short_name", lineNum) + + TimetableDirectionToggle.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (isChecked) { + when (checkedId) { + R.id.TimetableDirection1 -> { + animatedAlphaChange( + 0F, 1F, 0, TimetableLoadingIndicator ) - timetableDetailBundle.putInt( - "direction", - direction.direction_id + timetableDetailBundle.putString( + "long_name", directions[0].direction ) - timetableDetailBundle.putInt("station_id", stationId) - timetableDetailBundle.putString("station_name", stationName) - findNavController().navigate( - R.id.action_navigationTimetable_to_navigationTimetableDetail, + fetchTimetable( + timetablesViewModel, + routeId, + directions[0].direction_id, + idsbkSession, timetableDetailBundle ) } - val timetableAdapter = - TimetableAdapter(timetableData.departures[TimetableTimeSlider.value.toInt()]) { stationId, stationName -> - onTimetableStopClick( - stationId, - stationName - ) - } - TimetableStopsList.adapter = timetableAdapter - TimetableStopsList.layoutManager = LinearLayoutManager(context) - TimetableTimeSlider.addOnChangeListener { _, value, _ -> - if (value < timetableData.departures.size) - timetableAdapter.replaceList(timetableData.departures[value.toInt()]) - } - TimetableTimePlusButton.setOnClickListener { - val newValue = TimetableTimeSlider.value + 1F - if (newValue <= (departures.size - 1).toFloat()) - TimetableTimeSlider.value = newValue - } - TimetableTimeMinusButton.setOnClickListener { - val newValue = TimetableTimeSlider.value - 1F - if (newValue >= 0F) - TimetableTimeSlider.value = newValue + R.id.TimetableDirection2 -> { + animatedAlphaChange( + 0F, 1F, 0, TimetableLoadingIndicator + ) + timetableDetailBundle.putString( + "long_name", directions[1].direction + ) + fetchTimetable( + timetablesViewModel, + routeId, + directions[1].direction_id, + idsbkSession, + timetableDetailBundle + ) } - } else { - println("Error") // TODO: display error and retry button + } - animatedAlphaChange(1F, 0F, 100, TimetableLoadingIndicator) } } + TimetableDirectionToggle.clearChecked() + TimetableDirectionToggle.check(R.id.TimetableDirection1) + } else { + TimetableError.isVisible = true + } + } + } + } + } + + + private fun fetchTimetable( + timetablesViewModel: TimetablesViewModel, + routeId: Int, + direction: Int, + idsbkSession: Session, + timetableDetailBundle: Bundle + ) { + CoroutineScope(Dispatchers.IO).launch { + val timetable = timetablesViewModel.getTimetable( + routeId, "departures", direction, getDate(), idsbkSession + ) + val departures = timetable?.departures + binding.apply { + CoroutineScope(Dispatchers.Main).launch { + if (!departures.isNullOrEmpty()) { + val valueTo = (departures.size - 1).toFloat() + TimetableTimeSlider.valueFrom = 0F + TimetableTimeSlider.valueTo = if (valueTo > 0F) valueTo else 1F + TimetableTimeSlider.value = 0F + TimetableTimeSlider.stepSize = 1F + val minutes = getMinutes() + var tooLate = true + departures.forEachIndexed { i, d -> + if (d.departure - minutes > 1 && tooLate) { + tooLate = false + TimetableTimeSlider.value = i.toFloat() + } + } + if (tooLate) TimetableTimeSlider.value = (departures.size - 1).toFloat() + TimetableTimeSlider.setLabelFormatter { value -> + val departureTime = departures[value.toInt()].departure + return@setLabelFormatter "${departureTime / 60}:${ + (departureTime % 60).toString().padStart(2, Char(48)) + }" + } + + val timetableAdapter = + TimetableAdapter(departures[TimetableTimeSlider.value.toInt()]) { stationId, stationName -> + timetableDetailBundle.putInt("direction", direction) + timetableDetailBundle.putInt("station_id", stationId) + timetableDetailBundle.putString("station_name", stationName) + findNavController().navigate( + R.id.action_navigationTimetable_to_navigationTimetableDetail, + timetableDetailBundle + ) + } + TimetableStopsList.adapter = timetableAdapter + TimetableStopsList.layoutManager = LinearLayoutManager(context) + TimetableTimeSlider.addOnChangeListener { _, value, _ -> + if (value < departures.size) timetableAdapter.replaceList(departures[value.toInt()]) + } + TimetableTimePlusButton.setOnClickListener { + val newValue = TimetableTimeSlider.value + 1F + if (newValue <= (departures.size - 1).toFloat()) TimetableTimeSlider.value = + newValue + } + TimetableTimeMinusButton.setOnClickListener { + val newValue = TimetableTimeSlider.value - 1F + if (newValue >= 0F) TimetableTimeSlider.value = newValue + } + } else if (departures == null) { + TimetableError.isVisible = true + } else { + TimetableNoDeparturesInfo.isVisible = true } + animatedAlphaChange(1F, 0F, 100, TimetableLoadingIndicator) } } } diff --git a/app/src/main/java/eu/magicsk/transi/TimetablesFragment.kt b/app/src/main/java/eu/magicsk/transi/TimetablesFragment.kt index 0377a68..a43f199 100644 --- a/app/src/main/java/eu/magicsk/transi/TimetablesFragment.kt +++ b/app/src/main/java/eu/magicsk/transi/TimetablesFragment.kt @@ -15,7 +15,6 @@ import androidx.core.view.isVisible import androidx.core.view.setMargins import androidx.core.view.setPadding import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import eu.magicsk.transi.data.remote.responses.idsbk.Session @@ -59,9 +58,9 @@ class TimetablesFragment : Fragment() { mainViewModel.idsbkSession.observe(viewLifecycleOwner) { idsbkSession -> idsbkSession?.let { TimetablesErrorBtn.setOnClickListener { - fetchTimetables(timetablesViewModel, activity, idsbkSession) + fetchTimetables(timetablesViewModel, idsbkSession) } - fetchTimetables(timetablesViewModel, activity, idsbkSession) + fetchTimetables(timetablesViewModel, idsbkSession) } } } @@ -69,7 +68,6 @@ class TimetablesFragment : Fragment() { private fun fetchTimetables( timetablesViewModel: TimetablesViewModel, - activity: FragmentActivity?, idsbkSession: Session ) { binding.apply { @@ -145,7 +143,7 @@ class TimetablesFragment : Fragment() { ) } lineBtn.id = generateViewId() - activity?.runOnUiThread { + CoroutineScope(Dispatchers.Main).launch { when (line.route_type) { 0 -> TimetableTramsLines.addView(lineBtn) 2 -> TimetableTrainsLines.addView(lineBtn) @@ -169,12 +167,12 @@ class TimetablesFragment : Fragment() { } } else { - activity?.runOnUiThread { + CoroutineScope(Dispatchers.Main).launch { TimetablesError.isVisible = true TimetablesContent.isVisible = false } } - activity?.runOnUiThread { + CoroutineScope(Dispatchers.Main).launch { animatedAlphaChange(1F, 0F, 100, TimetablesLoadingIndicator) } } diff --git a/app/src/main/java/eu/magicsk/transi/view_models/TimetablesViewModel.kt b/app/src/main/java/eu/magicsk/transi/view_models/TimetablesViewModel.kt index 2db07fb..2433ce7 100755 --- a/app/src/main/java/eu/magicsk/transi/view_models/TimetablesViewModel.kt +++ b/app/src/main/java/eu/magicsk/transi/view_models/TimetablesViewModel.kt @@ -1,8 +1,6 @@ package eu.magicsk.transi.view_models -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import eu.magicsk.transi.data.remote.responses.idsbk.Session import eu.magicsk.transi.data.remote.responses.idsbk.Timetable @@ -11,7 +9,6 @@ import eu.magicsk.transi.data.remote.responses.idsbk.TimetableDirections import eu.magicsk.transi.data.remote.responses.idsbk.Timetables import eu.magicsk.transi.repository.DataRepository import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -20,24 +17,29 @@ class TimetablesViewModel @Inject constructor( private val repository: DataRepository ) : ViewModel() { - val timetablesDirections = MutableLiveData() - val timetable = MutableLiveData() - suspend fun getTimetables(session: Session): Timetables? = withContext(Dispatchers.IO) { val res = repository.getTimetables(session.session) res.data } - fun getTimetableDirections(route: Int, session: Session) = viewModelScope.launch { - val res = repository.getTimetableDirections(session.session, route) - res.data?.let { timetablesDirections.value = it } - } + suspend fun getTimetableDirections(route: Int, session: Session): TimetableDirections? = + withContext(Dispatchers.IO) { + val res = repository.getTimetableDirections(session.session, route) + res.data + } - fun getTimetable(route: Int, arrivalDeparture: String, direction: Int, date: String, session: Session) = - viewModelScope.launch { - val res = repository.getTimetable(session.session, route, arrivalDeparture, direction, date) - res.data?.let { timetable.value = it } + suspend fun getTimetable( + route: Int, + arrivalDeparture: String, + direction: Int, + date: String, + session: Session + ): Timetable? = + withContext(Dispatchers.IO) { + val res = + repository.getTimetable(session.session, route, arrivalDeparture, direction, date) + res.data } suspend fun getTimetableDetail( @@ -49,7 +51,14 @@ class TimetablesViewModel @Inject constructor( session: Session ): TimetableDetails? = withContext(Dispatchers.IO) { - val res = repository.getTimetableDetail(session.session, route, arrivalDeparture, direction, date, stop) + val res = repository.getTimetableDetail( + session.session, + route, + arrivalDeparture, + direction, + date, + stop + ) res.data } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_departure.xml b/app/src/main/res/drawable/ic_departure.xml new file mode 100644 index 0000000..c010392 --- /dev/null +++ b/app/src/main/res/drawable/ic_departure.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/fragment_timetable.xml b/app/src/main/res/layout/fragment_timetable.xml index d5989c5..2dfa728 100644 --- a/app/src/main/res/layout/fragment_timetable.xml +++ b/app/src/main/res/layout/fragment_timetable.xml @@ -1,6 +1,5 @@ - + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c6bb2cb..063b595 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -92,4 +92,6 @@ N999 Example XX + departureIcon + No routes at this time \ No newline at end of file