Filtrowanie listy z LiveData w metodzie observe oraz używanie liveData umieszczonego w companion object w Serwisie

0

Cześć.
Czy dobrym pomysłem jest używanie LiveData w ten sposób? Trzymanie pól typu LiveData w companion object zagnieżdżonego w TrackerService? Przeniesienie timera do bazy danych i załadowanie go do ViewModel powodowało opóźnienie. Jeśli miałoby to powodować jakieś problemy, to jak inaczej mogę przenieść dane z Service do ViewModel i umieścić je w LiveData?

@AndroidEntryPoint
class TrackerService: LifecycleService() {

    @Inject
    lateinit var notification: NotificationCompat.Builder
    @Inject
    lateinit var notificationManager: NotificationManager

    private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
    private val timer = Timer()

    companion object {
        val started = MutableLiveData<Boolean>()
        val distanceMeters = MutableLiveData<Double>()
        val runningTime = MutableLiveData<Int>()
        val date = MutableLiveData<Long>()
        val locationList = MutableLiveData<MutableList<LatLng>>()
        val kilometerReached = MutableLiveData<Boolean>()
        val paceTimes = MutableLiveData<MutableList<Int>>()
        val avgPaceTime = MutableLiveData<Double>()
        val burnedKcal = MutableLiveData<Int>()

        fun timerReset() {
            runTime.postValue(0)
        }
    }

[...]

Druga rzecz, z którą się zmagam, to filtrowanie listy z obserwowanych liveData. Czy dobrym pomysłem jest filtrowanie go w metodzie observe? Może powinienem to zrobić w inny sposób ponieważ np. grozi to zatrzymywaniem UI?

mainViewModel.readRuns.observe(viewLifecycleOwner) { runs ->
            mAdapter.setData(runs.sortedWith (
                when(historyViewModel.selectedChip){
                    binding.dateChip.id ->
                        if(desc) compareByDescending {it.date}
                        else compareBy {it.date}
                    binding.timeChip.id -> if(desc) compareByDescending {it.runTime}
                        else compareBy {it.runTime}
                    binding.distanceChip.id -> if(desc) compareByDescending {it.distanceMeters}
                        else compareBy {it.distanceMeters}
                    binding.caloriesChip.id -> if(desc) compareByDescending {it.burnedKcal}
                        else compareBy {it.burnedKcal}
                    else -> if(desc) compareByDescending {it.date} else compareBy {it.burnedKcal}
                }
            ))
        }
1

""Czy dobrym pomysłem jest używanie LiveData" tutaj już powinieneś uciąć pytanie. Odpowiedź brzmi nie.

Co chcesz zrobić? Pytam się od strony użytkownika a nie inżynierii. Nasłuchować na jakieś zmiany np z ~ActivityService?

0
lubie_programowac napisał(a):

""Czy dobrym pomysłem jest używanie LiveData" tutaj już powinieneś uciąć pytanie. Odpowiedź brzmi nie.

Co chcesz zrobić? Pytam się od strony użytkownika a nie inżynierii. Nasłuchować na jakieś zmiany np z ~ActivityService?

Nasłuchuję zmian we Fragmencie aby wyświetlać te dane w czasie rzeczywistym. Słusznie zwróciłeś uwagę i dwie z tych zmiennych wcale nie musiały być umieszczone w LiveData, ale dalej są zawarte w companion object abym mógł mieć do nich dostęp z tego fragmentu, ponieważ na input użytkownika są one zapisywane w bazie danych.

1

Wykorzystałbym tutaj podobny wzorzec jak przy komunikacji pomiędzy wieloma fragmentami czyli wspołdzielony view model. Tzn tutaj (viewmodel <-> serwis) nie będziemy korzystać stricte z viewmodelu ponieważ on powinien być bardziej do komunikacji z UIem. Wykorzystanie viewmodelu w serwisie nie wydaje sie naturalne. Możemy jednak wykorzystać inny obiekt np Repository. Czy przypadkiem odpowiedzialnosc repository (zgodnie z clean architecture) nie polaga na dostarczaniu danych z zrodla w agnostyczny sposob?

Jak mozna takie repository zaimplementowac. Mozna to zrobic przy pomocy wspodzielonego flow czyli zamaskowanego EventBusu https://medium.com/tech-takeaways/how-to-implement-the-event-bus-pattern-with-kotlin-sharedflow-in-your-android-app-768529828607 (trochę nie wierze że to pisze ale event bus wydaje sie lepszym pomyslem niz kilka livedata w companion object w serwisie)

Czyli jak to ogarnac, pseudokod:
Zrobić generycznego event busa:

class <T> EventBus {

    private val _events = MutableSharedFlow<T>()
    val events = _events.asSharedFlow()

    suspend fun invokeEvent(event: T) = _events.emit(event)
}

Zrobić instancje tego EventBus ktory bedzie singletonem przy uzyciu Hilta.
Wstrzyknac ta instancje do viemodelu oraz do serwisu.
W viewmodelu dac .collect na eventach
W serwisie robic invokeEvent.

Koniec

Tutaj jeszcze dochodza takie "szczegoly" jak obsluga dobrego coroutine scope, korzystanie bezposrednio z events, czy decyzja czy robimy jeden ~State na wszystkie przypadki z serwisu (co lubie ale to wtedy powinno byc chyba na innej warstwie, cos ala use case) czy przepychamy to tak jak u Ciebie w kilku polach.

Druga kwestia to live data: ona jest passé. Sam z niej korzystałem jeszcze kilka miesięcy temu do komunikacji viewmodel -> activity / fragment ale przeszedłem w 100% na flow i sobie chwale. Plus taki że ten sam mechanizm jest wykorzystywany na wszystkich warstwach.
Tutaj masz "mój" exension do flow, dzieki temu API jest niemal identyczne jak dla livedata

fun <T> StateFlow<T>.observe(scope: LifecycleCoroutineScope, callback: (T) -> Unit) {
    scope.launchWhenStarted {
        collect {
            callback.invoke(it)
        }
    }
}

Fragment:

 viewModel.myUiState.observe(lifecycleScope) {
   when(it) {
      is State.Init -> handleInit()
      is State.Loading -> handleLoading()
      is State.Fetched -> handleFetched(it.items)
   }
 }

View model

private val _myUiState = MutableStateFlow<State>(State.Init)
    val myUiState: StateFlow<State> = _myUiState

    fun getData(text: String) {
        _myUiState.value = State.Loading

        viewModelScope.launch {
            val items = useCase.execute(text)
            _myUiState.value = State.Fetched(items)
        }
    }
1

"Czy dobrym pomysłem jest filtrowanie go w metodzie observe?"
Nie, stan (w Twoim wypadku readRuns) powinien być tak przygotowany przez view model zeby od razu wyswietlic go na widoku. Filtrowanie na 100% powinno odbywac sie w VM.

Tutaj masz ok przyklad https://proandroiddev.com/traditional-mvvm-with-jetpack-compose-and-stateflow-7ca934e12784

sealed class WeatherUiState {
object Empty : WeatherUiState()
object Loading : WeatherUiState()
class Loaded(val data: WeatherUiModel) : WeatherUiState()
class Error(val message: String) : WeatherUiState()
}

U Ciebie w Loaded powinno byc cos ala ActivityUiModel ktory bedzie zawierac dane do pokazania. W VM ( docelowo w use casie) powinno byc robione filtrowanie.

Dobra ale jak ogarnac operacje w tle? Przy uzyciu korutyn sprawa wyglda prosto:

  1. Jak jestes w activity uzywasz lifecycleScope jak w przykladzie powyzej
  2. Fragment - tak samo
  3. W VM zawsze uzywasz viewModelScope.launch tak jak w przykladzie

1 użytkowników online, w tym zalogowanych: 0, gości: 1